diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
deleted file mode 100644
index 57547679..00000000
--- a/.github/ISSUE_TEMPLATE.md
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-### Info
-
-- Operating System:
-- Shell:
-- Terminal:
-- `nvim --version`:
-- `fzf --version`:
-
-
-- [ ] The issue is reproducible with `mini.sh`
-
-
-
-fzf-lua configuration
-
-
-
-```lua
-require('fzf-lua').setup({
-})
-```
-
-
-### Description
diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml
new file mode 100644
index 00000000..afb6b7ce
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug.yaml
@@ -0,0 +1,88 @@
+name: š Bug Report
+description: File a bug/issue
+labels: [bug]
+title: "Bug: "
+body:
+ - type: markdown
+ attributes:
+ value: |
+ **Before submitting an issue, please make sure to search the [Issues](https://github.com/ibhagwan/fzf-lua/issues)/[Discussions](https://github.com/ibhagwan/fzf-lua/discussions) and read the [Wiki](https://github.com/ibhagwan/fzf-lua/wiki) including the [Advanced section](https://github.com/ibhagwan/fzf-lua/wiki/Advanced) (if relevant)**
+
+ Many esoteric and lesser known options can be found by searching the above links, if you still have questions after that please use the [Discussions page](https://github.com/ibhagwan/fzf-lua/discussions), rest assured I will answer your quesion in the same priority as if it was an issue.
+ - type: checkboxes
+ attributes:
+ label: RTFM Checklist
+ description: |-
+ Please make sure to search the issues first and check only the relevant boxes below, if you don't think your issue is relevant to the Wiki/Advanced API or you aren't well versed in `fzf` (or unsure what that means) do not check these boxes, this mostly helps me understand where you're coming from so I can better assist you when troubleshooting.
+ options:
+ - label: I have searched exisiting issues / discussions
+ required: true
+ - label: I have read the Wiki including the Advanced section
+ required: false
+ - label: I have read `man fzf` / I am well versed in shell fzf
+ required: false
+ - type: input
+ attributes:
+ label: "Operating system"
+ placeholder: "e.g. Linux, Windows, Mac, etc"
+ validations:
+ required: true
+ - type: input
+ attributes:
+ label: "Shell"
+ placeholder: "e.g. bash, zsh, fish, etc"
+ validations:
+ required: true
+ - type: input
+ attributes:
+ label: "Neovim version (`nvim --version`)"
+ placeholder: "NVIM v0.10.0-dev-2739+ge2224a793"
+ validations:
+ required: true
+ - type: input
+ attributes:
+ label: "Fzf version (`fzf --version`)"
+ placeholder: "e.g. 0.48.1 (d579e33)"
+ validations:
+ required: true
+ - type: input
+ attributes:
+ label: "Output of `:lua print(os.getenv('FZF_DEFAULT_OPTS'))`"
+ placeholder: "e.g. --layout=reverse ..."
+ validations:
+ required: true
+ - type: checkboxes
+ attributes:
+ label: "Is the problem reproducible with `mini.sh`?"
+ description: |-
+ **Please mark a single checbox from the below options.**
+ Unless you're on Windows, you can quickly test fzf-lua with minimal config in its own sandbox by running the below command in the shell:
+ ```sh
+ sh -c "$(curl -s https://raw.githubusercontent.com/ibhagwan/fzf-lua/main/scripts/mini.sh)"
+ ```
+ options:
+ - label: My issue is reproducible with `mini.sh`
+ required: false
+ - label: My issue **IS NOT** reproducible with `mini.sh`
+ required: false
+ - label: I have not tested with `mini.sh` (not relevant, requires LSP, Windows, etc)
+ required: false
+
+ - type: textarea
+ id: config
+ attributes:
+ label: Fzf-lua configuration
+ description: Fzf-lua setup options
+ render: lua
+ value: |-
+ ```lua
+ require('fzf-lua').setup({
+ })
+ ```
+ - type: textarea
+ attributes:
+ label: Describe the bug / steps to reproduce
+ description: |-
+ Please describe your issue with as much details as you can, let me know what you've tested and your findings, bonus points if you can provide reproduction steps / code as this will help me get to the solution faster.
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..28b84a1a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Ask a question or start a discussion
+ url: https://github.com/ibhagwan/fzf-lua/discussions
+ about: Use Github discussions instead
diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml
new file mode 100644
index 00000000..79bcbfbb
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature.yaml
@@ -0,0 +1,26 @@
+name: š” Feature Request
+description: Suggest a new feature or improvement
+labels: [feature request]
+title: "Feature: "
+body:
+ - type: markdown
+ attributes:
+ value: |
+ **Before submitting a feature request, please make sure to search the [Issues](https://github.com/ibhagwan/fzf-lua/issues)/[Discussions](https://github.com/ibhagwan/fzf-lua/discussions) and read the [Wiki](https://github.com/ibhagwan/fzf-lua/wiki) including the [Advanced section](https://github.com/ibhagwan/fzf-lua/wiki/Advanced) (if relevant)**
+
+ Many esoteric and lesser known options can be found by searching the above links, if you still have questions after that please use the [Discussions page](https://github.com/ibhagwan/fzf-lua/discussions), rest assured I will answer your quesion in the same priority as if it was an issue.
+ - type: checkboxes
+ attributes:
+ label: Have you RTFM'd?
+ description: Please search the issues/discussions and read the Wiki first
+ options:
+ - label: I have done proper research
+ required: true
+ - type: textarea
+ id: feature
+ attributes:
+ label: Feature Request
+ description: |-
+ Please describe your request with as much details as you can. Helpful examples can include how your request works in other plugins/software as well as links to discussions/issues and implementation details if such exist.
+ validations:
+ required: true
diff --git a/.github/workflows/luarocks-release.yaml b/.github/workflows/luarocks-release.yaml
index a7b7567c..3569e043 100644
--- a/.github/workflows/luarocks-release.yaml
+++ b/.github/workflows/luarocks-release.yaml
@@ -16,7 +16,7 @@ jobs:
- name: Get new commit count
run: echo "NEW_COMMIT_COUNT=$(git log --oneline --since '24 hours ago' | wc -l)" >> $GITHUB_ENV
- name: LuaRocks Upload
- uses: nvim-neorocks/luarocks-tag-release@v3
+ uses: nvim-neorocks/luarocks-tag-release@v5
if: ${{ env.NEW_COMMIT_COUNT > 0 }}
env:
LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}
diff --git a/.github/workflows/sync_remote.yaml b/.github/workflows/sync_remote.yaml
new file mode 100644
index 00000000..9e4a5387
--- /dev/null
+++ b/.github/workflows/sync_remote.yaml
@@ -0,0 +1,23 @@
+on: [push, delete]
+
+jobs:
+ push:
+ runs-on: ubuntu-latest
+ if: github.repository == 'ibhagwan/fzf-lua'
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Sync remote repositories
+ run: |
+ mkdir -p ~/.ssh
+ echo "${{ secrets.GIT_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
+ chmod 600 ~/.ssh/id_rsa
+ # Ignore hosts keys, since we accept them as-is
+ git config --global core.sshCommand 'ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
+ # Codeberg
+ git remote add codeberg ssh://git@codeberg.org/ibhagwan/fzf-lua.git
+ git push --tags --force --prune codeberg 'refs/remotes/origin/*:refs/heads/*'
+ # GitLab
+ git remote add gitlab ssh://git@gitlab.com/ibhagwan/fzf-lua.git
+ git push --tags --force --prune gitlab 'refs/remotes/origin/*:refs/heads/*'
diff --git a/.luarc.json b/.luarc.json
deleted file mode 100644
index 124e64e5..00000000
--- a/.luarc.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "runtime.version": "LuaJIT",
- "diagnostics": {
- "enable": true,
- "globals": [
- "vim",
- "describe",
- "pending",
- "it",
- "before_each",
- "after_each",
- ],
- "neededFileStatus": {
- "codestyle-check": "Any"
- },
- "disable": [
- "need-check-nil",
- "missing-parameter",
- "cast-local-type",
- ],
- },
- "workspace": {
- "library": [
- "$VIMRUNTIME/lua",
- ],
- "checkThirdParty": false,
- "maxPreload": 2000,
- "preloadFileSize": 1000,
- "ignoreDir": [
- "tests/",
- ],
- },
- "type": {
- "weakNilCheck": true,
- "weakUnionCheck": true,
- "castNumberToInteger": true,
- },
- "telemetry.enable": false
-}
diff --git a/.luarc.jsonc b/.luarc.jsonc
new file mode 100644
index 00000000..6bc8764e
--- /dev/null
+++ b/.luarc.jsonc
@@ -0,0 +1,31 @@
+{
+ "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
+ "runtime.version": "LuaJIT",
+ "diagnostics": {
+ "enable": true,
+ "globals": ["vim"],
+ "neededFileStatus": {
+ "codestyle-check": "Any"
+ },
+ "disable": ["need-check-nil", "missing-parameter", "cast-local-type"]
+ },
+ "workspace": {
+ "library": [
+ "lua",
+ "$VIMRUNTIME/lua",
+ "${3rd}/luv/library",
+ "$XDG_DATA_HOME/nvim/lazy/plenary.nvim/lua",
+ "$LOCALAPPDATA/nvim-data/lazy/plenary.nvim/lua"
+ ],
+ "checkThirdParty": false,
+ "maxPreload": 2000,
+ "preloadFileSize": 1000,
+ "ignoreDir": ["tests/"]
+ },
+ "type": {
+ "weakNilCheck": true,
+ "weakUnionCheck": true,
+ "castNumberToInteger": true
+ },
+ "telemetry.enable": false
+}
diff --git a/Makefile b/Makefile
index 9bf4a56b..cca7b2d6 100644
--- a/Makefile
+++ b/Makefile
@@ -3,11 +3,12 @@ PLENARY-REPO=https://github.com/nvim-lua/plenary.nvim.git
.PHONY: test
test:
- nvim --headless --noplugin -u tests/minimal_init.vim -c "lua require('plenary.test_harness').test_directory('tests/', { minimal_init = 'tests/minimal_init.vim', sequential = true, timeout = 120000 })"
+ nvim --headless --noplugin -u tests/minimal_init.lua -c "lua require('plenary.test_harness').test_directory('tests/', { minimal_init = 'tests/minimal_init.lua', sequential = true, timeout = 120000 })"
+# run with `make FILE=tests/path_spec.lua test-file`
.PHONY: test-file
test-file:
- nvim --headless --noplugin -u tests/minimal_init.vim -c "lua require('plenary.busted').run(vim.loop.cwd()..'/'..[[$(FILE)]])"
+ nvim --headless --noplugin -u tests/minimal_init.lua -c "lua require('plenary.busted').run(vim.loop.cwd()..'/'..[[$(FILE)]])"
.PHONY: plenary
@@ -18,5 +19,5 @@ plenary:
git clone $(PLENARY-REPO) $(PLENARY-DIR); \
else \
echo "Updating plenary.nvim..."; \
- git -C $(PLENARY-DIR) pull; \
+ git -C $(PLENARY-DIR) pull --rebase; \
fi
diff --git a/README-Win.md b/README-Win.md
new file mode 100644
index 00000000..00100191
--- /dev/null
+++ b/README-Win.md
@@ -0,0 +1,57 @@
+## Windows Known Issues and Limitations
+
+As fzf-lua is bound by the same constraints, please read
+[fzf's Windows Wiki page](https://github.com/junegunn/fzf/wiki/Windows).
+
+It took a lot of work to make everything work exactly as it does on *NIX/OSX.
+Fzf-lua attempts to overcome inherent fzf Windows woes (escaping, etc) by using
+our command proxy wrapper and working around the issues using lua code.
+
+### Single quotes in commands / options
+
+On Windows, single quotes `'` in command arguments are treated as a string literal,
+that means that wrapping arguments with single quotes does not translate into a single
+string the same way a double quoted argument is treated, i.e. `'foo bar' != "foo bar"`.
+
+To avoid issues, make sure none of your `cmd`'s `rg_opts`, `fd_opts`, `preview`, etc
+contains single hyphens that should be treated as quotes, this is probably the case
+if you copied old fzf-lua defaults into your `setup` options.
+
+### live_grep_native
+
+When using `live_grep_native` we are sending the `rg` command directly
+to fzf (without the fzf-lua wrapper) and are therefore bound by fzf's escaping
+requirements.
+
+For example, `^` is a special escape character on windows and also a regex special
+character. Say we wanted to search for all lines that start with "local", we would
+run:
+```lua
+-- we use `no_esc` to tell rg we're using a regex
+:FzfLua live_grep no_esc=true search=^local
+```
+
+However, when using the native version we need to escape the caret twice:
+```lua
+:FzfLua live_grep no_esc=true search=^^local
+```
+
+More so, I couldn't find a way to send special regex chars `[(|.*^$` as the backslash
+is always doubled by fzf's `{q}`* see bottom note.
+
+For example, if we run:
+```cmd
+break | fzf --ansi --disabled --bind="change:reload:rg --line-number --column --color=always {q}"
+```
+
+And try to search for the literal `[` by typing `\[`, we get the error:
+```
+[Command failed: rg --line-number --column --color=always ^"\\[^"]
+```
+
+If we double the blackslashes by typing `\\[` we get the error:
+```
+[Command failed: rg --line-number --column --color=always ^"\\\\[^"]
+```
+
+* upstream issue: https://github.com/junegunn/fzf/issues/3626
diff --git a/README.md b/README.md
index c5b893f4..ef3ce4a9 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
![Neovim version](https://img.shields.io/badge/Neovim-0.5-57A143?style=flat-square&logo=neovim)
-[Quickstart](#quickstart) ā¢[Installation](#installation) ā¢ [Usage](#usage) ā¢ [Commands](#commands) ā¢ [Customization](#customization) ā¢ [Wiki](https://github.com/ibhagwan/fzf-lua/wiki)
+[Quickstart](#quickstart) ā¢ [Installation](#installation) ā¢ [Usage](#usage) ā¢ [Commands](#commands) ā¢ [Customization](#customization) ā¢ [Wiki](https://github.com/ibhagwan/fzf-lua/wiki)
![Demo](https://raw.githubusercontent.com/wiki/ibhagwan/fzf-lua/demo.gif)
@@ -20,6 +20,7 @@ yours too, if you allow it.
- [Why Fzf-lua](#why-fzf-lua)
- [Dependencies](#dependencies)
+ [Optional Dependencies](#optional-dependencies)
+ + [Windows Notes](#windows-notes)
- [Installation](#installation)
- [Usage](#usage)
+ [Resume](#resume)
@@ -103,7 +104,6 @@ at it. That, **and colorful file icons and git indicators!**.
## Dependencies
-- `Linux` or `MacOS`
- [`neovim`](https://github.com/neovim/neovim/releases) version > `0.5.0`
- [`fzf`](https://github.com/junegunn/fzf) version > `0.24`
**or** [`skim`](https://github.com/lotabout/skim) binary installed
@@ -128,6 +128,22 @@ to configure in `previewer.builtin.extensions`):
- [viu](https://github.com/atanunq/viu) - terminal image previewer
- [ueberzug](https://github.com/seebye/ueberzug) - X11 image previewer
+### Windows Notes
+
+- [rg](https://github.com/BurntSushi/ripgrep) is required for `grep` and `tags`
+- [git](https://git-scm.com/download/win) for Windows is required for `git`
+ (though installing `git-bash`|`sh` **is not required**).
+
+- Installation of dependencies (fzf, rg, fd, etc) is possible via
+ [scoop](https://github.com/ScoopInstaller/Install),
+ [chocolatey](https://chocolatey.org/install) or
+ [winget-cli](https://github.com/microsoft/winget-cli)
+
+- Although almost everything works on Windows exactly as the *NIX/OSX check out
+ the [Windows README](https://github.com/ibhagwan/fzf-lua/blob/main/README-Win.md)
+ for known issues and limitations.
+
+
## Installation
[![LuaRocks](https://img.shields.io/luarocks/v/ibhagwan/fzf-lua?logo=lua&color=purple)](https://luarocks.org/modules/ibhagwan/fzf-lua)
@@ -288,9 +304,9 @@ Alternatively, resuming work on a specific provider:
| `lsp_document_symbols` | Document Symbols |
| `lsp_workspace_symbols` | Workspace Symbols |
| `lsp_live_workspace_symbols` | Workspace Symbols (live query) |
-| `lsp_code_actions` | Code Actions |
| `lsp_incoming_calls` | Incoming Calls |
| `lsp_outgoing_calls` | Outgoing Calls |
+| `lsp_code_actions` | Code Actions |
| `lsp_finder` | All LSP locations, combined view |
| `diagnostics_document` | Document Diagnostics |
| `diagnostics_workspace` | Workspace Diagnostics |
@@ -300,27 +316,28 @@ Alternatively, resuming work on a specific provider:
### Misc
| Command | List |
| ---------------- | ------------------------------------------ |
-| `resume` | resume last command/query |
-| `builtin` | fzf-lua builtin commands |
-| `profiles` | fzf-lua configuration profiles |
-| `help_tags` | help tags |
-| `man_pages` | man pages |
-| `colorschemes` | color schemes |
-| `highlights` | highlight groups |
-| `commands` | neovim commands |
-| `command_history` | command history |
-| `search_history` | search history |
-| `marks` | :marks |
-| `jumps` | :jumps |
-| `changes` | :changes |
-| `registers` | :registers |
-| `tagstack` | :tags |
-| `autocmds` | :autocmd |
-| `keymaps` | key mappings |
-| `filetypes` | filetypes |
-| `menus` | menus |
-| `spell_suggest` | spelling suggestions |
-| `packadd` | :packadd |
+| `resume` | resume last command/query |
+| `builtin` | fzf-lua builtin commands |
+| `profiles` | fzf-lua configuration profiles |
+| `helptags` | help tags |
+| `manpages` | man pages |
+| `colorschemes` | color schemes |
+| `awesome_colorschemes` | Awesome Neovim color schemes |
+| `highlights` | highlight groups |
+| `commands` | neovim commands |
+| `command_history` | command history |
+| `search_history` | search history |
+| `marks` | :marks |
+| `jumps` | :jumps |
+| `changes` | :changes |
+| `registers` | :registers |
+| `tagstack` | :tags |
+| `autocmds` | :autocmd |
+| `keymaps` | key mappings |
+| `filetypes` | filetypes |
+| `menus` | menus |
+| `spell_suggest` | spelling suggestions |
+| `packadd` | :packadd |
### Neovim API
@@ -406,7 +423,7 @@ EOF
### Profiles
-Conviniently, fzf-lua comes with a set of preconfigured profiles, notably:
+Conveniently, fzf-lua comes with a set of preconfigured profiles, notably:
| Profile | Details |
| ---------------- | ------------------------------------------ |
| `default` | fzf-lua defaults, uses neovim "builtin" previewer and devicons (if available) for git/files/buffers |
@@ -431,6 +448,12 @@ telescope defaults with `bat` previewer:
:lua require"fzf-lua".setup({"telescope",winopts={preview={default="bat"}}})
```
+Combining of profiles is also available by sending table instead of string as
+the first argument:
+```lua
+:lua require"fzf-lua".setup({{"telescope","fzf-native"},winopts={fullscreen=true}})
+```
+
See [profiles](https://github.com/ibhagwan/fzf-lua/tree/main/lua/fzf-lua/profiles)
for more info.
@@ -639,10 +662,10 @@ require'fzf-lua'.setup {
fzf_opts = {
-- options are sent as `=`
-- set to `false` to remove a flag
- -- set to '' for a non-value flag
+ -- set to `true` for a no-value flag
-- for raw args use `fzf_args` instead
- ["--ansi"] = "",
- ["--info"] = "inline",
+ ["--ansi"] = true,
+ ["--info"] = "inline-right", -- fzf < v0.42 = "inline"
["--height"] = "100%",
["--layout"] = "reverse",
["--border"] = "none",
@@ -652,7 +675,19 @@ require'fzf-lua'.setup {
-- and removes the sides margin added by `fzf-tmux` (fzf#3162)
-- for more options run `fzf-tmux --help`
fzf_tmux_opts = { ["-p"] = "80%,80%", ["--margin"] = "0,0" },
- -- fzf '--color=' options (optional)
+ -- fzf's `--color=` arguments (optional)
+ -- If rhs is of type "string" rhs will be passed raw, e.g.:
+ -- `["fg"] = "underline"` will be translated to `--color fg:underline`
+ -- If rhs is of type "table", the following convention is used:
+ -- [1] "what" field to extract from the hlgroup, i.e "fg", "bg", etc.
+ -- [2] Neovim highlight group(s), can be either "string" or "table"
+ -- when type is "table" the first existing highlight group is used
+ -- [3+] any additional fields are passed raw to fzf's command line args
+ -- Example of a "fully loaded" color option:
+ -- `["fg"] = { "fg", { "NonExistentHl", "Comment" }, "underline", "bold" }`
+ -- Assuming `Comment.fg=#010101` the resulting fzf command line will be:
+ -- `--color fg:#010101:underline:bold`
+ -- NOTE: to pass raw arguments `fzf_opts["--color"]` or `fzf_args`
--[[ fzf_colors = {
["fg"] = { "fg", "CursorLine" },
["bg"] = { "bg", "Normal" },
@@ -671,7 +706,7 @@ require'fzf-lua'.setup {
previewers = {
cat = {
cmd = "cat",
- args = "--number",
+ args = "-n",
},
bat = {
cmd = "bat",
@@ -707,7 +742,7 @@ require'fzf-lua'.setup {
-- exclude specific filetypes with: `{ disable = { "lua" } }
-- disable fully with: `{ enable = false }`
treesitter = { enable = true, disable = {} },
- -- By default, the main window dimensions are calculted as if the
+ -- By default, the main window dimensions are calculated as if the
-- preview is visible, when hidden the main window will extend to
-- full size. Set the below to "extend" to prevent the main window
-- from being modified when toggling the preview.
@@ -751,7 +786,7 @@ require'fzf-lua'.setup {
},
-- PROVIDERS SETUP
-- use `defaults` (table or function) if you wish to set "global-provider" defaults
- -- for exmaple, disabling file icons globally and open the quickfix list at the top
+ -- for example, disabling file icons globally and open the quickfix list at the top
-- defaults = {
-- file_icons = false,
-- copen = "topleft copen",
@@ -772,8 +807,8 @@ require'fzf-lua'.setup {
-- NOTE: 'find -printf' requires GNU find
-- cmd = "find . -type f -printf '%P\n'",
find_opts = [[-type f -not -path '*/\.git/*' -printf '%P\n']],
- rg_opts = "--color=never --files --hidden --follow -g '!.git'",
- fd_opts = "--color=never --type f --hidden --follow --exclude .git",
+ rg_opts = [[--color=never --files --hidden --follow -g "!.git"]],
+ fd_opts = [[--color=never --type f --hidden --follow --exclude .git]],
-- by default, cwd appears in the header only if {opts} contain a cwd
-- parameter to a different folder than the current working directory
-- uncomment if you wish to force display of the cwd as part of the
@@ -786,11 +821,12 @@ require'fzf-lua'.setup {
actions = {
-- inherits from 'actions.files', here we can override
-- or set bind to 'false' to disable a default action
+ -- action to toggle `--no-ignore`, requires fd or rg installed
+ ["ctrl-g"] = { actions.toggle_ignore },
+ -- uncomment to override `actions.file_edit_or_qf`
-- ["default"] = actions.file_edit,
-- custom actions are available too
-- ["ctrl-y"] = function(selected) print(selected[1]) end,
- -- action to toggle `--no-ignore`, requires fd or rg installed
- -- ["ctrl-g"] = { actions.toggle_ignore },
}
},
git = {
@@ -801,13 +837,13 @@ require'fzf-lua'.setup {
git_icons = true, -- show git icons?
file_icons = true, -- show file icons?
color_icons = true, -- colorize file|git icons
- -- force display the cwd header line regardles of your current working
+ -- force display the cwd header line regardless of your current working
-- directory can also be used to hide the header when not wanted
-- cwd_header = true
},
status = {
prompt = 'GitStatusāÆ ',
- cmd = "git -c color.status=false status -su",
+ cmd = "git -c color.status=false --no-optional-locks status --porcelain=v1 -u",
multiprocess = true, -- run command in a separate process
file_icons = true,
git_icons = true,
@@ -832,7 +868,8 @@ require'fzf-lua'.setup {
},
commits = {
prompt = 'CommitsāÆ ',
- cmd = "git log --color --pretty=format:'%C(yellow)%h%Creset %Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset'",
+ cmd = [[git log --color --pretty=format:"%C(yellow)%h%Creset ]]
+ .. [[%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset"]],
preview = "git show --color {1}",
-- git-delta is automatically detected as pager, uncomment to disable
-- preview_pager = false,
@@ -849,7 +886,8 @@ require'fzf-lua'.setup {
-- git show --color {1} --rotate-to={file}
-- {1} : commit SHA (fzf field index expression)
-- {file} : filepath placement within the commands
- cmd = "git log --color --pretty=format:'%C(yellow)%h%Creset %Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset' {file}",
+ cmd = [[git log --color --pretty=format:"%C(yellow)%h%Creset ]]
+ .. [[%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset" {file}]],
preview = "git show --color {1} -- {file}",
-- git-delta is automatically detected as pager, uncomment to disable
-- preview_pager = false,
@@ -867,17 +905,24 @@ require'fzf-lua'.setup {
preview = "git log --graph --pretty=oneline --abbrev-commit --color {1}",
actions = {
["default"] = actions.git_switch,
+ ["ctrl-x"] = { fn = actions.git_branch_del, reload = true },
+ ["ctrl-a"] = { fn = actions.git_branch_add, field_index = "{q}", reload = true },
},
+ -- If you wish to add branch and switch immediately
+ -- cmd_add = { "git", "checkout", "-b" },
+ cmd_add = { "git", "branch" },
+ -- If you wish to delete unmerged branches add "--force"
+ -- cmd_del = { "git", "branch", "--delete", "--force" },
+ cmd_del = { "git", "branch", "--delete" },
},
tags = {
prompt = "Tags> ",
- cmd = "git for-each-ref --color --sort=-taggerdate --format "
- .. "'%(color:yellow)%(refname:short)%(color:reset) "
- .. "%(color:green)(%(taggerdate:relative))%(color:reset)"
- .. " %(subject) %(color:blue)%(taggername)%(color:reset)' refs/tags",
- preview = "git log --graph --color --pretty=format:'%C(yellow)%h%Creset "
- .. "%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset' {1}",
- fzf_opts = { ["--no-multi"] = "" },
+ cmd = [[git for-each-ref --color --sort="-taggerdate" --format ]]
+ .. [["%(color:yellow)%(refname:short)%(color:reset) ]]
+ .. [[%(color:green)(%(taggerdate:relative))%(color:reset)]]
+ .. [[ %(subject) %(color:blue)%(taggername)%(color:reset)" refs/tags]],
+ preview = [[git log --graph --color --pretty=format:"%C(yellow)%h%Creset ]]
+ .. [[%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset" {1}]],
actions = { ["default"] = actions.git_checkout },
},
stash = {
@@ -888,10 +933,6 @@ require'fzf-lua'.setup {
["default"] = actions.git_stash_apply,
["ctrl-x"] = { fn = actions.git_stash_drop, reload = true },
},
- fzf_opts = {
- ["--no-multi"] = '',
- ['--delimiter'] = "'[:]'",
- },
},
icons = {
["M"] = { icon = "M", color = "yellow" },
@@ -930,7 +971,7 @@ require'fzf-lua'.setup {
-- advanced usage: for custom argument parsing define
-- 'rg_glob_fn' to return a pair:
-- first returned argument is the new search query
- -- second returned argument are addtional rg flags
+ -- second returned argument are additional rg flags
-- rg_glob_fn = function(query, opts)
-- ...
-- return new_query, flags
@@ -939,6 +980,8 @@ require'fzf-lua'.setup {
-- actions inherit from 'actions.files' and merge
-- this action toggles between 'grep' and 'live_grep'
["ctrl-g"] = { actions.grep_lgrep }
+ -- uncomment to enable '.gitignore' toggle for grep
+ -- ["ctrl-r"] = { actions.toggle_ignore }
},
no_header = false, -- hide grep|cwd header?
no_header_i = false, -- hide interactive header?
@@ -985,7 +1028,7 @@ require'fzf-lua'.setup {
},
fzf_opts = {
-- hide tabnr
- ['--delimiter'] = "'[\\):]'",
+ ["--delimiter"] = "[\\):]",
["--with-nth"] = '2..',
},
},
@@ -998,7 +1041,7 @@ require'fzf-lua'.setup {
fzf_opts = {
-- do not include bufnr in fuzzy matching
-- tiebreak by line no.
- ['--delimiter'] = "'[\\]:]'",
+ ["--delimiter"] = "[\\]:]",
["--nth"] = '2..',
["--tiebreak"] = 'index',
["--tabstop"] = "1",
@@ -1018,7 +1061,7 @@ require'fzf-lua'.setup {
-- start = "cursor" -- start display from cursor?
fzf_opts = {
-- hide filename, tiebreak by line no.
- ["--delimiter"] = "'[:]'",
+ ["--delimiter"] = "[:]",
["--with-nth"] = '2..',
["--tiebreak"] = 'index',
["--tabstop"] = "1",
@@ -1040,7 +1083,7 @@ require'fzf-lua'.setup {
-- 'tags_live_grep' options, `rg` prioritizes over `grep`
rg_opts = "--no-heading --color=always --smart-case",
grep_opts = "--color=auto --perl-regexp",
- fzf_opts = { ["--info"] = "default", ["--tiebreak"] = "begin" },
+ fzf_opts = { ["--tiebreak"] = "begin" },
actions = {
-- actions inherit from 'actions.files' and merge
-- this action toggles between 'grep' and 'live_grep'
@@ -1052,25 +1095,47 @@ require'fzf-lua'.setup {
btags = {
prompt = 'BTagsāÆ ',
ctags_file = nil, -- auto-detect from tags-option
- ctags_autogen = false, -- dynamically generate ctags each call
+ ctags_autogen = true, -- dynamically generate ctags each call
multiprocess = true,
file_icons = false,
git_icons = false,
rg_opts = "--color=never --no-heading",
grep_opts = "--color=never --perl-regexp",
- fzf_opts = { ["--info"] = "default", ["--tiebreak"] = "begin" },
+ fzf_opts = { ["--tiebreak"] = "begin" },
-- actions inherit from 'actions.files'
},
colorschemes = {
prompt = 'ColorschemesāÆ ',
live_preview = true, -- apply the colorscheme on preview?
- actions = { ["default"] = actions.colorscheme, },
+ actions = { ["default"] = actions.colorscheme },
winopts = { height = 0.55, width = 0.30, },
-- uncomment to ignore colorschemes names (lua patterns)
-- ignore_patterns = { "^delek$", "^blue$" },
- -- uncomment to execute a callback after interface is closed
+ -- uncomment to execute a callback on preview|close
-- e.g. a call to reset statusline highlights
- -- post_reset_cb = function() ... end,
+ -- cb_preview = function() ... end,
+ -- cb_exit = function() ... end,
+ },
+ awesome_colorschemes = {
+ prompt = 'ColorschemesāÆ ',
+ live_preview = true, -- apply the colorscheme on preview?
+ max_threads = 5, -- max download/update threads
+ winopts = { row = 0, col = 0.99, width = 0.50 },
+ fzf_opts = {
+ ["--multi"] = true,
+ ["--delimiter"] = "[:]",
+ ["--with-nth"] = "3..",
+ ["--tiebreak"] = "index",
+ },
+ actions = {
+ ["default"] = actions.colorscheme,
+ ["ctrl-g"] = { fn = actions.toggle_bg, exec_silent = true },
+ ["ctrl-r"] = { fn = actions.cs_update, reload = true },
+ ["ctrl-x"] = { fn = actions.cs_delete, reload = true },
+ },
+ -- uncomment to execute a callback on preview|close
+ -- cb_preview = function() ... end,
+ -- cb_exit = function() ... end,
},
keymaps = {
prompt = "Keymaps> ",
@@ -1089,6 +1154,7 @@ require'fzf-lua'.setup {
quickfix = {
file_icons = true,
git_icons = true,
+ only_valid = false, -- select among only the valid quickfix entries
},
quickfix_stack = {
prompt = "Quickfix Stack> ",
@@ -1150,10 +1216,7 @@ require'fzf-lua'.setup {
symbol_fmt = function(s, opts) return "[" .. s .. "]" end,
-- prefix child symbols. set to any string or `false` to disable
child_prefix = true,
- fzf_opts = {
- ["--tiebreak"] = "begin",
- ["--info"] = "default",
- },
+ fzf_opts = { ["--tiebreak"] = "begin" },
},
code_actions = {
prompt = 'Code Actions> ',
@@ -1165,7 +1228,6 @@ require'fzf-lua'.setup {
},
finder = {
prompt = "LSP Finder> ",
- fzf_opts = { ["--info"] = "default" },
file_icons = true,
color_icons = true,
git_icons = false,
@@ -1237,15 +1299,7 @@ require'fzf-lua'.setup {
-- manpages = { previewer = "man_native" },
-- helptags = { previewer = "help_native" },
--
- -- optional override of file extension icon colors
- -- available colors (terminal):
- -- clear, bold, black, red, green, yellow
- -- blue, magenta, cyan, grey, dark_grey, white
- file_icon_colors = {
- ["sh"] = "green",
- },
- -- padding can help kitty term users with
- -- double-width icon rendering
+ -- padding can help kitty term users with double-width icon rendering
file_icon_padding = '',
-- uncomment if your terminal/font does not support unicode character
-- 'EN SPACE' (U+2002), the below sets it to 'NBSP' (U+00A0) instead
@@ -1257,7 +1311,7 @@ require'fzf-lua'.setup {
### Highlights
-FzfLua conviniently creates the below highlights, each hlgroup can be
+FzfLua conveniently creates the below highlights, each hlgroup can be
temporarily overridden by its corresponding `winopts` option:
| Highlight Group | Default | Override Via | Notes |
@@ -1288,6 +1342,7 @@ temporarily overridden by its corresponding `winopts` option:
|FzfLuaTabTitle |*LightSkyBlue1 |`hls.tab_title` |Tab title (`tabs`)|
|FzfLuaTabMarker |*BlanchedAlmond |`hls.tab_marker` |Tab marker (`tabs`)|
|FzfLuaDirIcon |Directory |`hls.dir_icon` |Paths directory icon|
+|FzfLuaLiveSym |*Brown1 |`hls.live_sym` |LSP live symbols query match|
*Not a highlight group, RGB color from `nvim_get_color_map`
diff --git a/autoload/fzf_lua.vim b/autoload/fzf_lua.vim
new file mode 100644
index 00000000..c827d3ef
--- /dev/null
+++ b/autoload/fzf_lua.vim
@@ -0,0 +1,28 @@
+" Calling vim.fn.getbufinfo in lua can be expensive because it has to convert
+" all the buffer variables into lua values. Since fzf-lua does not access
+" buffer variables, this cost can be avoided by clearing the entry before
+" passing the info to lua.
+function! fzf_lua#getbufinfo(bufnr) abort
+ let info = getbufinfo(a:bufnr)
+ if empty(info)
+ return [] " there is no way to return `nil` from vimscript
+ endif
+ let vars = info[0].variables
+ unlet! info[0].variables
+ " Make sure we copy 'current_syntax' as `utils.buf_is_qf`
+ " uses it to detect quickfix buffers
+ if !empty(vars) && has_key(vars, "current_syntax")
+ let info[0].variables = { "current_syntax": vars.current_syntax }
+ endif
+ return info[0]
+endfunction
+
+" Similar to fzf_lua#getbufinfo, but for getwininfo.
+function! fzf_lua#getwininfo(winid) abort
+ let info = getwininfo(a:winid)
+ if empty(info)
+ return []
+ endif
+ unlet! info[0].variables
+ return info[0]
+endfunction
diff --git a/doc/fzf-lua.txt b/doc/fzf-lua.txt
index 3d47c44c..064b35cb 100644
--- a/doc/fzf-lua.txt
+++ b/doc/fzf-lua.txt
@@ -1,4 +1,4 @@
-*fzf-lua.txt* For Neovim >= 0.5.0 Last change: 2024 January 05
+*fzf-lua.txt* For Neovim >= 0.5.0 Last change: 2024 April 08
==============================================================================
Table of Contents *fzf-lua-table-of-contents*
@@ -9,6 +9,7 @@ Rationale ................................................ |fzf-lua-rationale|
Why Fzf-Lua ............................................ |fzf-lua-why-fzf-lua|
Dependencies .......................................... |fzf-lua-dependencies|
Optional dependencies ........................ |fzf-lua-optional-dependencies|
+Windows Notes ........................................ |fzf-lua-windows-notes|
Installation .......................................... |fzf-lua-installation|
Usage ........................................................ |fzf-lua-usage|
Resume ...................................................... |fzf-lua-resume|
@@ -38,7 +39,7 @@ FZF :HEART: LUA *fzf-lua-fzf-:heart:-lua*
Neovim version
-Quickstart <#quickstart> ā¢Installation <#installation> ā¢ Usage <#usage>
+Quickstart <#quickstart> ā¢ Installation <#installation> ā¢ Usage <#usage>
ā¢ Commands <#commands> ā¢ Customization <#customization> ā¢ Wiki
@@ -56,7 +57,7 @@ CONTENTS *fzf-lua-contents*
- Rationale <#rationale>
- Why Fzf-lua <#why-fzf-lua>
- Dependencies <#dependencies> + Optional Dependencies
- <#optional-dependencies>
+ <#optional-dependencies> + Windows Notes <#windows-notes>
- Installation <#installation>
- Usage <#usage> + Resume <#resume>
- Commands <#commands> + Buffers and Files <#buffers-and-files> + Search
@@ -144,7 +145,6 @@ throw at it. That, **and colorful file icons and git indicators!**.
DEPENDENCIES *fzf-lua-dependencies*
-- `Linux` or `MacOS`
- `neovim` version > `0.5.0`
- `fzf` version > `0.24` **or** `skim`
binary installed
@@ -170,6 +170,21 @@ to configure in `previewer.builtin.extensions`):
- viu - terminal image previewer
- ueberzug - X11 image previewer
+
+WINDOWS NOTES *fzf-lua-windows-notes*
+
+- rg is required for `grep` and `tags`
+- git for Windows is required for `git`
+ (though installing `git-bash`|`sh` **is not required**).
+- Installation of dependencies (fzf, rg, fd, etc) is possible via scoop
+ , chocolatey
+ or winget-cli
+
+- Although almost everything works on Windows exactly as the *NIX/OSX check
+ out the Windows README
+ for known
+ issues and limitations.
+
------------------------------------------------------------------------------
INSTALLATION *fzf-lua-installation*
@@ -353,9 +368,9 @@ LSP/DIAGNOSTICS *fzf-lua-lsp/diagnostics*
| `lsp_document_symbols` | Document Symbols |
| `lsp_workspace_symbols` | Workspace Symbols |
| `lsp_live_workspace_symbols` | Workspace Symbols (live query) |
-| `lsp_code_actions` | Code Actions |
| `lsp_incoming_calls` | Incoming Calls |
| `lsp_outgoing_calls` | Outgoing Calls |
+| `lsp_code_actions` | Code Actions |
| `lsp_finder` | All LSP locations, combined view |
| `diagnostics_document` | Document Diagnostics |
| `diagnostics_workspace` | Workspace Diagnostics |
@@ -368,27 +383,28 @@ MISC *fzf-lua-misc*
| Command | List |
| ---------------- | ------------------------------------------ |
-| `resume` | resume last command/query |
-| `builtin` | fzf-lua builtin commands |
-| `profiles` | fzf-lua configuration profiles |
-| `help_tags` | help tags |
-| `man_pages` | man pages |
-| `colorschemes` | color schemes |
-| `highlights` | highlight groups |
-| `commands` | neovim commands |
-| `command_history` | command history |
-| `search_history` | search history |
-| `marks` | :marks |
-| `jumps` | :jumps |
-| `changes` | :changes |
-| `registers` | :registers |
-| `tagstack` | :tags |
-| `autocmds` | :autocmd |
-| `keymaps` | key mappings |
-| `filetypes` | filetypes |
-| `menus` | menus |
-| `spell_suggest` | spelling suggestions |
-| `packadd` | :packadd |
+| `resume` | resume last command/query |
+| `builtin` | fzf-lua builtin commands |
+| `profiles` | fzf-lua configuration profiles |
+| `helptags` | help tags |
+| `manpages` | man pages |
+| `colorschemes` | color schemes |
+| `awesome_colorschemes` | Awesome Neovim color schemes |
+| `highlights` | highlight groups |
+| `commands` | neovim commands |
+| `command_history` | command history |
+| `search_history` | search history |
+| `marks` | :marks |
+| `jumps` | :jumps |
+| `changes` | :changes |
+| `registers` | :registers |
+| `tagstack` | :tags |
+| `autocmds` | :autocmd |
+| `keymaps` | key mappings |
+| `filetypes` | filetypes |
+| `menus` | menus |
+| `spell_suggest` | spelling suggestions |
+| `packadd` | :packadd |
@@ -489,7 +505,7 @@ Can also be called from a `.vim` file:
PROFILES *fzf-lua-profiles*
-Conviniently, fzf-lua comes with a set of preconfigured profiles, notably:
+Conveniently, fzf-lua comes with a set of preconfigured profiles, notably:
| Profile | Details |
| ---------------- | ------------------------------------------ |
@@ -516,6 +532,12 @@ telescope defaults with `bat` previewer:
>lua
:lua require"fzf-lua".setup({"telescope",winopts={preview={default="bat"}}})
<
+Combining of profiles is also available by sending table instead of string as
+the first argument:
+
+>lua
+ :lua require"fzf-lua".setup({{"telescope","fzf-native"},winopts={fullscreen=true}})
+<
See profiles
for more
info.
@@ -732,10 +754,10 @@ open an issue and I'll be more than happy to help.**
fzf_opts = {
-- options are sent as `=`
-- set to `false` to remove a flag
- -- set to '' for a non-value flag
+ -- set to `true` for a no-value flag
-- for raw args use `fzf_args` instead
- ["--ansi"] = "",
- ["--info"] = "inline",
+ ["--ansi"] = true,
+ ["--info"] = "inline-right", -- fzf < v0.42 = "inline"
["--height"] = "100%",
["--layout"] = "reverse",
["--border"] = "none",
@@ -745,7 +767,19 @@ open an issue and I'll be more than happy to help.**
-- and removes the sides margin added by `fzf-tmux` (fzf#3162)
-- for more options run `fzf-tmux --help`
fzf_tmux_opts = { ["-p"] = "80%,80%", ["--margin"] = "0,0" },
- -- fzf '--color=' options (optional)
+ -- fzf's `--color=` arguments (optional)
+ -- If rhs is of type "string" rhs will be passed raw, e.g.:
+ -- `["fg"] = "underline"` will be translated to `--color fg:underline`
+ -- If rhs is of type "table", the following convention is used:
+ -- [1] "what" field to extract from the hlgroup, i.e "fg", "bg", etc.
+ -- [2] Neovim highlight group(s), can be either "string" or "table"
+ -- when type is "table" the first existing highlight group is used
+ -- [3+] any additional fields are passed raw to fzf's command line args
+ -- Example of a "fully loaded" color option:
+ -- `["fg"] = { "fg", { "NonExistentHl", "Comment" }, "underline", "bold" }`
+ -- Assuming `Comment.fg=#010101` the resulting fzf command line will be:
+ -- `--color fg:#010101:underline:bold`
+ -- NOTE: to pass raw arguments `fzf_opts["--color"]` or `fzf_args`
--[[ fzf_colors = {
["fg"] = { "fg", "CursorLine" },
["bg"] = { "bg", "Normal" },
@@ -764,7 +798,7 @@ open an issue and I'll be more than happy to help.**
previewers = {
cat = {
cmd = "cat",
- args = "--number",
+ args = "-n",
},
bat = {
cmd = "bat",
@@ -800,7 +834,7 @@ open an issue and I'll be more than happy to help.**
-- exclude specific filetypes with: `{ disable = { "lua" } }
-- disable fully with: `{ enable = false }`
treesitter = { enable = true, disable = {} },
- -- By default, the main window dimensions are calculted as if the
+ -- By default, the main window dimensions are calculated as if the
-- preview is visible, when hidden the main window will extend to
-- full size. Set the below to "extend" to prevent the main window
-- from being modified when toggling the preview.
@@ -844,7 +878,7 @@ open an issue and I'll be more than happy to help.**
},
-- PROVIDERS SETUP
-- use `defaults` (table or function) if you wish to set "global-provider" defaults
- -- for exmaple, disabling file icons globally and open the quickfix list at the top
+ -- for example, disabling file icons globally and open the quickfix list at the top
-- defaults = {
-- file_icons = false,
-- copen = "topleft copen",
@@ -865,8 +899,8 @@ open an issue and I'll be more than happy to help.**
-- NOTE: 'find -printf' requires GNU find
-- cmd = "find . -type f -printf '%P\n'",
find_opts = [[-type f -not -path '*/\.git/*' -printf '%P\n']],
- rg_opts = "--color=never --files --hidden --follow -g '!.git'",
- fd_opts = "--color=never --type f --hidden --follow --exclude .git",
+ rg_opts = [[--color=never --files --hidden --follow -g "!.git"]],
+ fd_opts = [[--color=never --type f --hidden --follow --exclude .git]],
-- by default, cwd appears in the header only if {opts} contain a cwd
-- parameter to a different folder than the current working directory
-- uncomment if you wish to force display of the cwd as part of the
@@ -879,11 +913,12 @@ open an issue and I'll be more than happy to help.**
actions = {
-- inherits from 'actions.files', here we can override
-- or set bind to 'false' to disable a default action
+ -- action to toggle `--no-ignore`, requires fd or rg installed
+ ["ctrl-g"] = { actions.toggle_ignore },
+ -- uncomment to override `actions.file_edit_or_qf`
-- ["default"] = actions.file_edit,
-- custom actions are available too
-- ["ctrl-y"] = function(selected) print(selected[1]) end,
- -- action to toggle `--no-ignore`, requires fd or rg installed
- -- ["ctrl-g"] = { actions.toggle_ignore },
}
},
git = {
@@ -894,13 +929,13 @@ open an issue and I'll be more than happy to help.**
git_icons = true, -- show git icons?
file_icons = true, -- show file icons?
color_icons = true, -- colorize file|git icons
- -- force display the cwd header line regardles of your current working
+ -- force display the cwd header line regardless of your current working
-- directory can also be used to hide the header when not wanted
-- cwd_header = true
},
status = {
prompt = 'GitStatusāÆ ',
- cmd = "git -c color.status=false status -su",
+ cmd = "git -c color.status=false --no-optional-locks status --porcelain=v1 -u",
multiprocess = true, -- run command in a separate process
file_icons = true,
git_icons = true,
@@ -925,7 +960,8 @@ open an issue and I'll be more than happy to help.**
},
commits = {
prompt = 'CommitsāÆ ',
- cmd = "git log --color --pretty=format:'%C(yellow)%h%Creset %Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset'",
+ cmd = [[git log --color --pretty=format:"%C(yellow)%h%Creset ]]
+ .. [[%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset"]],
preview = "git show --color {1}",
-- git-delta is automatically detected as pager, uncomment to disable
-- preview_pager = false,
@@ -942,7 +978,8 @@ open an issue and I'll be more than happy to help.**
-- git show --color {1} --rotate-to={file}
-- {1} : commit SHA (fzf field index expression)
-- {file} : filepath placement within the commands
- cmd = "git log --color --pretty=format:'%C(yellow)%h%Creset %Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset' {file}",
+ cmd = [[git log --color --pretty=format:"%C(yellow)%h%Creset ]]
+ .. [[%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset" {file}]],
preview = "git show --color {1} -- {file}",
-- git-delta is automatically detected as pager, uncomment to disable
-- preview_pager = false,
@@ -960,17 +997,24 @@ open an issue and I'll be more than happy to help.**
preview = "git log --graph --pretty=oneline --abbrev-commit --color {1}",
actions = {
["default"] = actions.git_switch,
+ ["ctrl-x"] = { fn = actions.git_branch_del, reload = true },
+ ["ctrl-a"] = { fn = actions.git_branch_add, field_index = "{q}", reload = true },
},
+ -- If you wish to add branch and switch immediately
+ -- cmd_add = { "git", "checkout", "-b" },
+ cmd_add = { "git", "branch" },
+ -- If you wish to delete unmerged branches add "--force"
+ -- cmd_del = { "git", "branch", "--delete", "--force" },
+ cmd_del = { "git", "branch", "--delete" },
},
tags = {
prompt = "Tags> ",
- cmd = "git for-each-ref --color --sort=-taggerdate --format "
- .. "'%(color:yellow)%(refname:short)%(color:reset) "
- .. "%(color:green)(%(taggerdate:relative))%(color:reset)"
- .. " %(subject) %(color:blue)%(taggername)%(color:reset)' refs/tags",
- preview = "git log --graph --color --pretty=format:'%C(yellow)%h%Creset "
- .. "%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset' {1}",
- fzf_opts = { ["--no-multi"] = "" },
+ cmd = [[git for-each-ref --color --sort="-taggerdate" --format ]]
+ .. [["%(color:yellow)%(refname:short)%(color:reset) ]]
+ .. [[%(color:green)(%(taggerdate:relative))%(color:reset)]]
+ .. [[ %(subject) %(color:blue)%(taggername)%(color:reset)" refs/tags]],
+ preview = [[git log --graph --color --pretty=format:"%C(yellow)%h%Creset ]]
+ .. [[%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset" {1}]],
actions = { ["default"] = actions.git_checkout },
},
stash = {
@@ -981,10 +1025,6 @@ open an issue and I'll be more than happy to help.**
["default"] = actions.git_stash_apply,
["ctrl-x"] = { fn = actions.git_stash_drop, reload = true },
},
- fzf_opts = {
- ["--no-multi"] = '',
- ['--delimiter'] = "'[:]'",
- },
},
icons = {
["M"] = { icon = "M", color = "yellow" },
@@ -1023,7 +1063,7 @@ open an issue and I'll be more than happy to help.**
-- advanced usage: for custom argument parsing define
-- 'rg_glob_fn' to return a pair:
-- first returned argument is the new search query
- -- second returned argument are addtional rg flags
+ -- second returned argument are additional rg flags
-- rg_glob_fn = function(query, opts)
-- ...
-- return new_query, flags
@@ -1032,6 +1072,8 @@ open an issue and I'll be more than happy to help.**
-- actions inherit from 'actions.files' and merge
-- this action toggles between 'grep' and 'live_grep'
["ctrl-g"] = { actions.grep_lgrep }
+ -- uncomment to enable '.gitignore' toggle for grep
+ -- ["ctrl-r"] = { actions.toggle_ignore }
},
no_header = false, -- hide grep|cwd header?
no_header_i = false, -- hide interactive header?
@@ -1078,7 +1120,7 @@ open an issue and I'll be more than happy to help.**
},
fzf_opts = {
-- hide tabnr
- ['--delimiter'] = "'[\\):]'",
+ ["--delimiter"] = "[\\):]",
["--with-nth"] = '2..',
},
},
@@ -1091,7 +1133,7 @@ open an issue and I'll be more than happy to help.**
fzf_opts = {
-- do not include bufnr in fuzzy matching
-- tiebreak by line no.
- ['--delimiter'] = "'[\\]:]'",
+ ["--delimiter"] = "[\\]:]",
["--nth"] = '2..',
["--tiebreak"] = 'index',
["--tabstop"] = "1",
@@ -1111,7 +1153,7 @@ open an issue and I'll be more than happy to help.**
-- start = "cursor" -- start display from cursor?
fzf_opts = {
-- hide filename, tiebreak by line no.
- ["--delimiter"] = "'[:]'",
+ ["--delimiter"] = "[:]",
["--with-nth"] = '2..',
["--tiebreak"] = 'index',
["--tabstop"] = "1",
@@ -1133,7 +1175,7 @@ open an issue and I'll be more than happy to help.**
-- 'tags_live_grep' options, `rg` prioritizes over `grep`
rg_opts = "--no-heading --color=always --smart-case",
grep_opts = "--color=auto --perl-regexp",
- fzf_opts = { ["--info"] = "default", ["--tiebreak"] = "begin" },
+ fzf_opts = { ["--tiebreak"] = "begin" },
actions = {
-- actions inherit from 'actions.files' and merge
-- this action toggles between 'grep' and 'live_grep'
@@ -1145,25 +1187,47 @@ open an issue and I'll be more than happy to help.**
btags = {
prompt = 'BTagsāÆ ',
ctags_file = nil, -- auto-detect from tags-option
- ctags_autogen = false, -- dynamically generate ctags each call
+ ctags_autogen = true, -- dynamically generate ctags each call
multiprocess = true,
file_icons = false,
git_icons = false,
rg_opts = "--color=never --no-heading",
grep_opts = "--color=never --perl-regexp",
- fzf_opts = { ["--info"] = "default", ["--tiebreak"] = "begin" },
+ fzf_opts = { ["--tiebreak"] = "begin" },
-- actions inherit from 'actions.files'
},
colorschemes = {
prompt = 'ColorschemesāÆ ',
live_preview = true, -- apply the colorscheme on preview?
- actions = { ["default"] = actions.colorscheme, },
+ actions = { ["default"] = actions.colorscheme },
winopts = { height = 0.55, width = 0.30, },
-- uncomment to ignore colorschemes names (lua patterns)
-- ignore_patterns = { "^delek$", "^blue$" },
- -- uncomment to execute a callback after interface is closed
+ -- uncomment to execute a callback on preview|close
-- e.g. a call to reset statusline highlights
- -- post_reset_cb = function() ... end,
+ -- cb_preview = function() ... end,
+ -- cb_exit = function() ... end,
+ },
+ awesome_colorschemes = {
+ prompt = 'ColorschemesāÆ ',
+ live_preview = true, -- apply the colorscheme on preview?
+ max_threads = 5, -- max download/update threads
+ winopts = { row = 0, col = 0.99, width = 0.50 },
+ fzf_opts = {
+ ["--multi"] = true,
+ ["--delimiter"] = "[:]",
+ ["--with-nth"] = "3..",
+ ["--tiebreak"] = "index",
+ },
+ actions = {
+ ["default"] = actions.colorscheme,
+ ["ctrl-g"] = { fn = actions.toggle_bg, exec_silent = true },
+ ["ctrl-r"] = { fn = actions.cs_update, reload = true },
+ ["ctrl-x"] = { fn = actions.cs_delete, reload = true },
+ },
+ -- uncomment to execute a callback on preview|close
+ -- cb_preview = function() ... end,
+ -- cb_exit = function() ... end,
},
keymaps = {
prompt = "Keymaps> ",
@@ -1182,6 +1246,7 @@ open an issue and I'll be more than happy to help.**
quickfix = {
file_icons = true,
git_icons = true,
+ only_valid = false, -- select among only the valid quickfix entries
},
quickfix_stack = {
prompt = "Quickfix Stack> ",
@@ -1243,10 +1308,7 @@ open an issue and I'll be more than happy to help.**
symbol_fmt = function(s, opts) return "[" .. s .. "]" end,
-- prefix child symbols. set to any string or `false` to disable
child_prefix = true,
- fzf_opts = {
- ["--tiebreak"] = "begin",
- ["--info"] = "default",
- },
+ fzf_opts = { ["--tiebreak"] = "begin" },
},
code_actions = {
prompt = 'Code Actions> ',
@@ -1258,7 +1320,6 @@ open an issue and I'll be more than happy to help.**
},
finder = {
prompt = "LSP Finder> ",
- fzf_opts = { ["--info"] = "default" },
file_icons = true,
color_icons = true,
git_icons = false,
@@ -1330,15 +1391,7 @@ open an issue and I'll be more than happy to help.**
-- manpages = { previewer = "man_native" },
-- helptags = { previewer = "help_native" },
--
- -- optional override of file extension icon colors
- -- available colors (terminal):
- -- clear, bold, black, red, green, yellow
- -- blue, magenta, cyan, grey, dark_grey, white
- file_icon_colors = {
- ["sh"] = "green",
- },
- -- padding can help kitty term users with
- -- double-width icon rendering
+ -- padding can help kitty term users with double-width icon rendering
file_icon_padding = '',
-- uncomment if your terminal/font does not support unicode character
-- 'EN SPACE' (U+2002), the below sets it to 'NBSP' (U+00A0) instead
@@ -1349,7 +1402,7 @@ open an issue and I'll be more than happy to help.**
HIGHLIGHTS *fzf-lua-highlights*
-FzfLua conviniently creates the below highlights, each hlgroup can be
+FzfLua conveniently creates the below highlights, each hlgroup can be
temporarily overridden by its corresponding `winopts` option:
| Highlight Group | Default | Override Via | Notes |
@@ -1380,6 +1433,7 @@ temporarily overridden by its corresponding `winopts` option:
|FzfLuaTabTitle |*LightSkyBlue1 |`hls.tab_title` |Tab title (`tabs`)|
|FzfLuaTabMarker |*BlanchedAlmond |`hls.tab_marker` |Tab marker (`tabs`)|
|FzfLuaDirIcon |Directory |`hls.dir_icon` |Paths directory icon|
+|FzfLuaLiveSym |*Brown1 |`hls.live_sym` |LSP live symbols query match|
*Not a highlight group, RGB color from
`nvim_get_color_map`
diff --git a/lua/fzf-lua/actions.lua b/lua/fzf-lua/actions.lua
index 3831e2be..2b25169d 100644
--- a/lua/fzf-lua/actions.lua
+++ b/lua/fzf-lua/actions.lua
@@ -16,7 +16,7 @@ M.expect = function(actions)
end
end
if #keys > 0 then
- return string.format("--expect=%s", table.concat(keys, ","))
+ return table.concat(keys, ",")
end
return nil
end
@@ -96,7 +96,7 @@ M.vimcmd = function(vimcmd, selected, noesc)
end
end
-M.vimcmd_file = function(vimcmd, selected, opts)
+M.vimcmd_file = function(vimcmd, selected, opts, pcall_vimcmd)
local curbuf = vim.api.nvim_buf_get_name(0)
local is_term = utils.is_term_buffer(0)
for i = 1, #selected do
@@ -104,7 +104,7 @@ M.vimcmd_file = function(vimcmd, selected, opts)
if entry.path == "" then goto continue end
entry.ctag = opts._ctag and path.entry_to_ctag(selected[i])
local fullpath = entry.path or entry.uri and entry.uri:match("^%a+://(.*)")
- if not path.starts_with_separator(fullpath) then
+ if not path.is_absolute(fullpath) then
fullpath = path.join({ opts.cwd or opts._cwd or vim.loop.cwd(), fullpath })
end
if vimcmd == "e"
@@ -125,15 +125,23 @@ M.vimcmd_file = function(vimcmd, selected, opts)
-- add current location to jumplist
if not is_term then vim.cmd("normal! m`") end
-- only change buffer if we need to (issue #122)
- if vimcmd ~= "e" or curbuf ~= fullpath then
+ if vimcmd ~= "e" or not path.equals(curbuf, fullpath) then
if entry.path then
-- do not run ': ' for uri entries (#341)
- local relpath = path.relative(entry.path, vim.loop.cwd())
+ local relpath = path.relative_to(entry.path, vim.loop.cwd())
if vim.o.autochdir then
-- force full paths when `autochdir=true` (#882)
relpath = fullpath
end
- vim.cmd(vimcmd .. " " .. vim.fn.fnameescape(relpath))
+ -- we normalize the path or Windows will fail with directories starting
+ -- with special characters, for example "C:\app\(web)" will be traslated
+ -- by neovim to "c:\app(web)" (#1082)
+ local cmd = vimcmd .. " " .. vim.fn.fnameescape(path.normalize(relpath))
+ if pcall_vimcmd then
+ pcall(vim.cmd, cmd)
+ else
+ vim.cmd(cmd)
+ end
elseif vimcmd ~= "e" then
-- uri entries only execute new buffers (new|vnew|tabnew)
vim.cmd(vimcmd)
@@ -188,7 +196,7 @@ local sel_to_qf = function(selected, opts, is_loclist)
local file = path.entry_to_file(selected[i], opts)
local text = selected[i]:match(":%d+:%d?%d?%d?%d?:?(.*)$")
table.insert(qf_list, {
- filename = file.bufname or file.path,
+ filename = file.bufname or file.path or file.uri,
lnum = file.line,
col = file.col,
text = text,
@@ -245,7 +253,7 @@ M.file_switch = function(selected, opts)
local bufnr = nil
local entry = path.entry_to_file(selected[1])
local fullpath = entry.path
- if not path.starts_with_separator(fullpath) then
+ if not path.is_absolute(fullpath) then
fullpath = path.join({ opts.cwd or vim.loop.cwd(), fullpath })
end
for _, b in ipairs(vim.api.nvim_list_bufs()) do
@@ -299,6 +307,10 @@ M.vimcmd_buf = function(vimcmd, selected, opts)
-- add current location to jumplist
if not is_term then vim.cmd("normal! m`") end
if vimcmd ~= "b" or curbuf ~= entry.bufnr then
+ if vimcmd == "bd" and utils.is_term_bufname(entry.bufname) then
+ -- killing terminal buffers requires ! (#1078)
+ vimcmd = vimcmd .. "!"
+ end
local cmd = vimcmd .. " " .. entry.bufnr
local ok, res = pcall(vim.cmd, cmd)
if not ok then
@@ -383,37 +395,38 @@ M.buf_edit_or_qf = function(selected, opts)
end
end
-M.colorscheme = function(selected)
- local colorscheme = selected[1]
- vim.cmd("colorscheme " .. colorscheme)
- -- setup fzf-lua's own highlight groups
- utils.setup_highlights()
+M.colorscheme = function(selected, opts)
+ local dbkey, idx = selected[1]:match("^(.-):(%d+):")
+ if dbkey then
+ opts._apply_awesome_theme(dbkey, idx, opts)
+ else
+ local colorscheme = selected[1]:match("^[^:]+")
+ pcall(function() vim.cmd("colorscheme " .. colorscheme) end)
+ end
+end
+
+M.cs_delete = function(selected, opts)
+ for _, s in ipairs(selected) do
+ local dbkey = s:match("^(.-):%d+:")
+ opts._adm:delete(dbkey)
+ end
+end
+
+M.cs_update = function(selected, opts)
+ local dedup = {}
+ for _, s in ipairs(selected) do
+ local dbkey = s:match("^(.-):%d+:")
+ if dbkey then dedup[dbkey] = true end
+ end
+ for k, _ in pairs(dedup) do
+ opts._adm:update(k)
+ end
end
-M.ensure_insert_mode = function()
- -- not sure what is causing this, tested with
- -- 'NVIM v0.6.0-dev+575-g2ef9d2a66'
- -- vim.cmd("startinsert") doesn't start INSERT mode
- -- 'mode' returns { blocking = false, mode = "t" }
- -- manually input 'i' seems to workaround this issue
- -- **only if fzf term window was succefully opened (#235)
- -- this is only required after the 'nt' (normal-terminal)
- -- mode was introduced along with the 'ModeChanged' event
- -- https://github.com/neovim/neovim/pull/15878
- -- https://github.com/neovim/neovim/pull/15840
- -- local has_mode_nt = not vim.tbl_isempty(
- -- vim.fn.getcompletion('ModeChanged', 'event'))
- -- or vim.fn.has('nvim-0.6') == 1
- -- if has_mode_nt then
- -- local mode = vim.api.nvim_get_mode()
- -- local wininfo = vim.fn.getwininfo(vim.api.nvim_get_current_win())[1]
- -- if vim.bo.ft == 'fzf'
- -- and wininfo.terminal == 1
- -- and mode and mode.mode == 't' then
- -- vim.cmd[[noautocmd lua vim.api.nvim_feedkeys('i', 'n', true)]]
- -- end
- -- end
- utils.warn("calling 'ensure_insert_mode' is no longer required and can be safely omitted.")
+M.toggle_bg = function(_, _)
+ vim.o.background = vim.o.background == "dark" and "light" or "dark"
+ utils.setup_highlights()
+ utils.info(string.format([[background set to "%s"]], vim.o.background))
end
M.run_builtin = function(selected)
@@ -507,7 +520,7 @@ M.spell_apply = function(selected)
end
M.set_filetype = function(selected)
- vim.api.nvim_buf_set_option(0, "filetype", selected[1])
+ vim.bo.filetype = selected[1]
end
M.packadd = function(selected)
@@ -538,9 +551,7 @@ M.help_tab = function(selected)
end
local function mantags(s)
- return vim.tbl_map(function(x)
- return x:match("[^[,( ]+")
- end, s)
+ return vim.tbl_map(require("fzf-lua.providers.manpages").manpage_vim_arg, s)
end
M.man = function(selected)
@@ -560,6 +571,7 @@ end
M.git_switch = function(selected, opts)
+ if not selected[1] then return end
local cmd = path.git_cwd({ "git", "checkout" }, opts)
local git_ver = utils.git_version()
-- git switch was added with git version 2.23
@@ -579,8 +591,46 @@ M.git_switch = function(selected, opts)
utils.err(unpack(output))
else
utils.info(unpack(output))
- if #vim.api.nvim_buf_get_name(0) > 0 then
- vim.cmd("edit!")
+ vim.cmd("checktime")
+ end
+end
+
+M.git_branch_add = function(selected, opts)
+ -- "reload" actions (fzf version >= 0.36) use field_index = "{q}"
+ -- so the prompt input will be found in `selected[1]`
+ -- previous fzf versions (or skim) restart the process instead
+ -- so the prompt input will be found in `opts.last_query`
+ local branch = opts.last_query or selected[1]
+ if type(branch) ~= "string" or #branch == 0 then
+ utils.warn("Branch name cannot be empty, use prompt for input.")
+ else
+ local cmd_add_branch = path.git_cwd(opts.cmd_add, opts)
+ table.insert(cmd_add_branch, branch)
+ local output, rc = utils.io_systemlist(cmd_add_branch)
+ if rc ~= 0 then
+ utils.err(unpack(output))
+ else
+ utils.info(string.format("Created branch '%s'.", branch))
+ end
+ end
+end
+
+M.git_branch_del = function(selected, opts)
+ local cmd_del_branch = path.git_cwd(opts.cmd_del, opts)
+ local cmd_cur_branch = path.git_cwd({ "git", "rev-parse", "--abbrev-ref", "HEAD" }, opts)
+ local branch = selected[1]:match("[^%s%*]+")
+ local cur_branch = utils.io_systemlist(cmd_cur_branch)[1]
+ if branch == cur_branch then
+ utils.warn(string.format("Cannot delete active branch '%s'", branch))
+ return
+ end
+ if utils.input("Delete branch " .. branch .. "? [y/n] ") == "y" then
+ table.insert(cmd_del_branch, branch)
+ local output, rc = utils.io_systemlist(cmd_del_branch)
+ if rc ~= 0 then
+ utils.err(unpack(output))
+ else
+ utils.info(unpack(output))
end
end
end
@@ -607,21 +657,19 @@ M.git_yank_commit = function(selected, opts)
end
M.git_checkout = function(selected, opts)
- local cmd_checkout = path.git_cwd({ "git", "checkout" }, opts)
- local cmd_cur_commit = path.git_cwd({ "git", "rev-parse", "--short HEAD" }, opts)
+ local cmd_cur_commit = path.git_cwd({ "git", "rev-parse", "--short", "HEAD" }, opts)
local commit_hash = match_commit_hash(selected[1], opts)
+ local current_commit = utils.io_systemlist(cmd_cur_commit)[1]
+ if commit_hash == current_commit then return end
if utils.input("Checkout commit " .. commit_hash .. "? [y/n] ") == "y" then
- local current_commit = utils.io_systemlist(cmd_cur_commit)
- if (commit_hash == current_commit) then return end
+ local cmd_checkout = path.git_cwd({ "git", "checkout" }, opts)
table.insert(cmd_checkout, commit_hash)
local output, rc = utils.io_systemlist(cmd_checkout)
if rc ~= 0 then
utils.err(unpack(output))
else
utils.info(unpack(output))
- if #vim.api.nvim_buf_get_name(0) > 0 then
- vim.cmd("edit!")
- end
+ vim.cmd("checktime")
end
end
end
@@ -629,7 +677,7 @@ end
local git_exec = function(selected, opts, cmd, silent)
local success
for _, e in ipairs(selected) do
- local file = path.relative(path.entry_to_file(e, opts).path, opts.cwd)
+ local file = path.relative_to(path.entry_to_file(e, opts).path, opts.cwd)
local _cmd = vim.deepcopy(cmd)
table.insert(_cmd, file)
local output, rc = utils.io_systemlist(_cmd)
@@ -678,6 +726,8 @@ M.git_reset = function(selected, opts)
and path.git_cwd({ "git", "clean", "-f" }, opts)
or path.git_cwd({ "git", "checkout", "HEAD", "--" }, opts)
git_exec({ s }, opts, cmd)
+ -- trigger autoread or warn the users buffer(s) was changed
+ vim.cmd("checktime")
end
end
@@ -690,7 +740,8 @@ M.git_stash_pop = function(selected, opts)
if utils.input("Pop " .. #selected .. " stash(es)? [y/n] ") == "y" then
local cmd = path.git_cwd({ "git", "stash", "pop" }, opts)
git_exec(selected, opts, cmd)
- vim.cmd("e!")
+ -- trigger autoread or warn the users buffer(s) was changed
+ vim.cmd("checktime")
end
end
@@ -698,7 +749,8 @@ M.git_stash_apply = function(selected, opts)
if utils.input("Apply " .. #selected .. " stash(es)? [y/n] ") == "y" then
local cmd = path.git_cwd({ "git", "stash", "apply" }, opts)
git_exec(selected, opts, cmd)
- vim.cmd("e!")
+ -- trigger autoread or warn the users buffer(s) was changed
+ vim.cmd("checktime")
end
end
@@ -707,7 +759,7 @@ M.git_buf_edit = function(selected, opts)
local git_root = path.git_root(opts, true)
local win = vim.api.nvim_get_current_win()
local buffer_filetype = vim.bo.filetype
- local file = path.relative(vim.fn.expand("%:p"), git_root)
+ local file = path.relative_to(path.normalize(vim.fn.expand("%:p")), git_root)
local commit_hash = match_commit_hash(selected[1], opts)
table.insert(cmd, commit_hash .. ":" .. file)
local git_file_contents = utils.io_systemlist(cmd)
@@ -715,10 +767,10 @@ M.git_buf_edit = function(selected, opts)
local file_name = string.gsub(file, "$", "[" .. commit_hash .. "]")
vim.api.nvim_buf_set_lines(buf, 0, 0, true, git_file_contents)
vim.api.nvim_buf_set_name(buf, file_name)
- vim.api.nvim_buf_set_option(buf, "buftype", "nofile")
- vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe")
- vim.api.nvim_buf_set_option(buf, "filetype", buffer_filetype)
- vim.api.nvim_buf_set_option(buf, "modifiable", false)
+ vim.bo[buf].buftype = "nofile"
+ vim.bo[buf].bufhidden = "wipe"
+ vim.bo[buf].modifiable = false
+ vim.bo[buf].filetype = buffer_filetype
vim.api.nvim_win_set_buf(win, buf)
end
@@ -744,7 +796,9 @@ end
M.arg_del = function(selected, opts)
local vimcmd = "argdel"
- M.vimcmd_file(vimcmd, selected, opts)
+ -- since we don't dedup argdel can fail if file is added
+ -- more than once into the arglist
+ M.vimcmd_file(vimcmd, selected, opts, true)
end
M.grep_lgrep = function(_, opts)
@@ -766,21 +820,22 @@ M.sym_lsym = function(_, opts)
end
M.toggle_ignore = function(_, opts)
- local o = { resume = true }
+ local o = { resume = true, cwd = opts.cwd }
local flag = opts.toggle_ignore_flag or "--no-ignore"
if not flag:match("^%s") then
-- flag must be preceded by whitespace
flag = " " .. flag
end
- if opts.cmd:match(utils.lua_regex_escape(flag)) then
- o._hdr_to = false
- o.cmd = opts.cmd:gsub(utils.lua_regex_escape(flag), "")
+ -- grep|live_grep sets `opts._cmd` to the original
+ -- command without the search argument
+ local cmd = opts._cmd or opts.cmd
+ if cmd:match(utils.lua_regex_escape(flag)) then
+ o.cmd = cmd:gsub(utils.lua_regex_escape(flag), "")
else
- -- signals "core.set_header" to set the correct "to" header
- o._hdr_to = true
- o.cmd = opts.cmd .. flag
+ local bin, args = cmd:match("([^%s]+)(.*)$")
+ o.cmd = string.format("%s%s%s", bin, flag, args)
end
- opts.__ACT_TO(o)
+ opts.__call_fn(o)
end
M.tmux_buf_set_reg = function(selected, opts)
@@ -813,12 +868,15 @@ M.set_qflist = function(selected, opts)
vim.cmd(opts._is_loclist and "lopen" or "copen")
end
+---@param selected string[]
+---@param opts table
M.apply_profile = function(selected, opts)
- local fname = selected[1]:match("[^:]+")
- local profile = selected[1]:match(":([^%s]+)")
- local ok = utils.load_profile(fname, profile, opts.silent)
+ local entry = path.entry_to_file(selected[1])
+ local fname = entry.path
+ local profile = entry.stripped:sub(#fname + 2):match("[^%s]+")
+ local ok = utils.load_profile_fname(fname, profile, opts.silent)
if ok then
- vim.cmd(string.format([[lua require("fzf-lua").setup({"%s"})]], profile))
+ loadstring(string.format([[require("fzf-lua").setup({"%s"})]], profile))()
end
end
@@ -841,4 +899,28 @@ M.complete = function(selected, opts)
end
end
+M.dap_bp_del = function(selected, opts)
+ local bufnrs = {}
+ local dap_bps = require("dap.breakpoints")
+ for _, e in ipairs(selected) do
+ local entry = path.entry_to_file(e, opts)
+ if entry.bufnr > 0 and entry.line then
+ dap_bps.remove(entry.bufnr, entry.line)
+ table.insert(bufnrs, tonumber(entry.bufnr))
+ end
+ end
+ -- removing the BP will update the UI, if we're in session
+ -- we also need to broadcast the BP delete to the DAP server
+ local session = require("dap").session()
+ if session then
+ local bps = dap_bps.get()
+ for _, b in ipairs(bufnrs) do
+ -- If all BPs were removed from a buffer we must clear the buffer
+ -- by sending an empty table in the bufnr index
+ bps[b] = bps[b] or {}
+ end
+ session:set_breakpoints(bps)
+ end
+end
+
return M
diff --git a/lua/fzf-lua/complete.lua b/lua/fzf-lua/complete.lua
index f63ec21e..e270a736 100644
--- a/lua/fzf-lua/complete.lua
+++ b/lua/fzf-lua/complete.lua
@@ -1,5 +1,6 @@
local core = require "fzf-lua.core"
local path = require "fzf-lua.path"
+local utils = require "fzf-lua.utils"
local config = require "fzf-lua.config"
local M = {}
@@ -23,14 +24,14 @@ local function find_toplevel_cwd(maybe_cwd, postfix, orig_cwd)
if vim.fn.isdirectory(vim.fn.expand(maybe_cwd)) == 1 then
local disp_cwd, cwd = maybe_cwd, vim.fn.expand(maybe_cwd)
-- returned cwd must be full path
- if cwd:sub(1, 1) == "." and cwd:sub(2, 2) == path.SEPARATOR then
+ if path.has_cwd_prefix(cwd) then
cwd = vim.loop.cwd() .. (#cwd > 1 and cwd:sub(2) or "")
-- inject "./" only if original path started with it
-- otherwise ignore the "." retval from fnamemodify
if #orig_cwd > 0 and orig_cwd:sub(1, 1) ~= "." then
disp_cwd = nil
end
- elseif not path.starts_with_separator(cwd) then
+ elseif not path.is_absolute(cwd) then
cwd = path.join({ vim.loop.cwd(), cwd })
end
return disp_cwd, cwd, postfix
@@ -60,15 +61,13 @@ local set_cmp_opts_path = function(opts)
if not opts.prompt then
opts.prompt = "."
end
- if not path.ends_with_separator(opts.prompt) then
- opts.prompt = opts.prompt .. path.SEPARATOR
- end
+ opts.prompt = path.add_trailing(opts.prompt)
-- completion function rebuilds the line with the full path
opts.complete = function(selected, o, l, _)
-- query fuzzy matching is empty
if #selected == 0 then return end
local replace_at = col - #before
- local relpath = path.relative(path.entry_to_file(selected[1], o).path, opts.cwd)
+ local relpath = path.relative_to(path.entry_to_file(selected[1], o).path, opts.cwd)
local before_path = replace_at > 1 and l:sub(1, replace_at - 1) or ""
local rest_of_line = #l >= (col + #after) and l:sub(col + #after) or ""
local resolved_path = opts._cwd and path.join({ opts._cwd, relpath }) or relpath
@@ -87,8 +86,8 @@ M.path = function(opts)
return "fdfind"
elseif vim.fn.executable("fd") == 1 then
return "fd"
- elseif vim.fn.executable("rg") == 1 then
- return "rg --files"
+ elseif utils.__IS_WINDOWS then
+ return "dir /s/b"
else
return [[find ! -path '.' ! -path '*/\.git/*' -printf '%P\n']]
end
@@ -103,12 +102,14 @@ M.file = function(opts)
if not opts then return end
opts.cmp_is_file = true
opts.cmd = opts.cmd or (function()
- if vim.fn.executable("rg") == 1 then
- return "rg --files"
- elseif vim.fn.executable("fdfind") == 1 then
+ if vim.fn.executable("fdfind") == 1 then
return "fdfind --type f --exclude .git"
elseif vim.fn.executable("fd") == 1 then
return "fd --type f --exclude .git"
+ elseif vim.fn.executable("rg") == 1 then
+ return "rg --files"
+ elseif utils.__IS_WINDOWS then
+ return "dir /s/b"
else
return [[find -type f ! -path '*/\.git/*' -printf '%P\n']]
end
@@ -123,7 +124,7 @@ M.line = function(opts)
opts.query = (function()
local col = vim.api.nvim_win_get_cursor(0)[2] + 1
local line = vim.api.nvim_get_current_line()
- return #line > col and vim.trim(line:sub(1, col)) or nil
+ return vim.trim(line:sub(1, col))
end)()
opts.complete = function(selected, _, _, _)
local newline = selected[1]:match("^.*:%d+:%s(.*)")
diff --git a/lua/fzf-lua/config.lua b/lua/fzf-lua/config.lua
index 7b3954a5..293ef951 100644
--- a/lua/fzf-lua/config.lua
+++ b/lua/fzf-lua/config.lua
@@ -1,77 +1,10 @@
local path = require "fzf-lua.path"
local utils = require "fzf-lua.utils"
local actions = require "fzf-lua.actions"
+local devicons = require "fzf-lua.devicons"
local M = {}
-if utils.__HAS_DEVICONS then
- M._has_devicons, M._devicons = pcall(require, "nvim-web-devicons")
-
- -- get the devicons module path
- M._devicons_path = M._has_devicons and M._devicons and M._devicons.setup
- and debug.getinfo(M._devicons.setup, "S").source:gsub("^@", "")
-end
-
-M._diricon_escseq = function()
- local hlgroup = utils.map_get(M, "__resume_data.opts.hls.dir_icon") or M.globals.__HLS.dir_icon
- local _, escseq = utils.ansi_from_hl(hlgroup)
- return escseq
-end
-
--- get icons proxy for the headless instance
-M._devicons_geticons = function()
- if not M._has_devicons or not M._devicons or not M._devicons.get_icons then
- return
- end
- -- force refresh if `bg` changed from dark/light (#855)
- if M.__DEVICONS and vim.o.bg == M.__DEVICONS_BG then
- return M.__DEVICONS
- end
- -- save the current background
- M.__DEVICONS_BG = vim.o.bg
- -- rpc request cannot return a table that has mixed elements
- -- of both indexed items and key value, it will fail with
- -- "Cannot convert given lua table"
- -- NOTES:
- -- (1) devicons.get_icons() returns the default icon in [1]
- -- (2) we cannot rely on having either .name or .color (#817)
- local all_devicons = M._devicons.get_icons()
- if not all_devicons or vim.tbl_isempty(all_devicons) then
- -- something is wrong with devicons
- -- can't use `error` due to fast event
- print("[Fzf-lua] error: devicons.get_icons() is nil or empty!")
- return
- end
- -- We only need the name, icon and color properties
- local default_icon = all_devicons[1] or {}
- M.__DEVICONS = {
- [""] = {
- name = default_icon.name or "Default",
- icon = default_icon.icon or "ļ¶",
- color = default_icon.color or "#6d8086",
- }
- }
- for k, v in pairs(all_devicons) do
- -- skip all indexed (numeric) entries
- if type(k) == "string" then
- M.__DEVICONS[k] = {
- name = v.name or k,
- icon = v.icon or "ļ¶",
- color = v.color or (function()
- -- some devicons customizations remove `info.color`
- -- retrieve the color from the highlight group (#801)
- local hlgroup = "DevIcon" .. (v.name or k)
- local hexcol = utils.hexcol_from_hl(hlgroup, "fg")
- if hexcol and #hexcol > 0 then
- return hexcol
- end
- end)(),
- }
- end
- end
- return M.__DEVICONS
-end
-
-- set this so that make_entry won't get nil err when setting remotely
M.__resume_data = {}
@@ -116,10 +49,14 @@ M.resume_set = function(what, val, opts)
-- backward compatibility for users using `get_last_query`
if what == "query" then
utils.map_set(M, "__resume_data.last_query", val)
+ -- store in opts for convinience in action callbacks
+ opts.last_query = val
end
-- _G.dump("resume_set", key1, utils.map_get(M, key1))
end
+---@param opts {resume: boolean, __call_opts: table}
+---@return table
function M.resume_opts(opts)
assert(opts.resume and opts.__call_opts)
local __call_opts = M.resume_get(nil, opts)
@@ -191,6 +128,9 @@ do
m.globals = M.globals
end
+---@param opts table|fun():table?
+---@param globals string|table?
+---@param __resume_key string?
function M.normalize_opts(opts, globals, __resume_key)
if not opts then opts = {} end
@@ -210,6 +150,7 @@ function M.normalize_opts(opts, globals, __resume_key)
-- save the user's original call params separately
opts.__call_opts = opts.__call_opts or utils.deepcopy(opts)
+ opts.__call_fn = utils.__FNCREF2__()
-- resume storage data lookup key, default to the calling function ref
-- __FNCREF2__ will use the 2nd function ref in the stack (calling fn)
@@ -235,6 +176,8 @@ function M.normalize_opts(opts, globals, __resume_key)
end
-- normalize all binds as lowercase or we can have duplicate keys (#654)
+ ---@param m {fzf: table, builtin: table}
+ ---@return {fzf: table, builtin: table}?
local keymap_tolower = function(m)
return m and {
fzf = utils.map_tolower(m.fzf),
@@ -256,7 +199,7 @@ function M.normalize_opts(opts, globals, __resume_key)
-- Merge required tables from globals
for _, k in ipairs({
- "winopts", "keymap", "fzf_opts", "fzf_tmux_opts", "previewers", "hls"
+ "winopts", "keymap", "fzf_opts", "fzf_tmux_opts", "hls"
}) do
opts[k] = vim.tbl_deep_extend("keep",
-- must clone or map will be saved as reference
@@ -266,11 +209,37 @@ function M.normalize_opts(opts, globals, __resume_key)
type(M.globals[k]) == "table" and utils.tbl_deep_clone(M.globals[k]) or {})
end
+ -- backward compat: no-value flags should be set to `true`, in the past these
+ -- would be set to an empty string which would now translate into a shell escaped
+ -- string as we automatically shell escape all fzf_opts
+ for k, v in pairs(opts.fzf_opts) do
+ if v == "" then opts.fzf_opts[k] = true end
+ end
+
+ -- Execlude file icons from the fuzzy matching (#1080)
+ if opts.file_icons and opts._fzf_nth_devicons and not opts.fzf_opts["--delimiter"] then
+ opts.fzf_opts["--nth"] = opts.fzf_opts["--nth"] or "-1.."
+ opts.fzf_opts["--delimiter"] = string.format("[%s]", utils.nbsp)
+ end
+
+ -- fzf.vim's `g:fzf_history_dir` (#1127)
+ if vim.g.fzf_history_dir and opts.fzf_opts["--history"] == nil then
+ local histdir = vim.fn.expand(vim.g.fzf_history_dir)
+ if vim.fn.isdirectory(histdir) == 0 then
+ pcall(vim.fn.mkdir, histdir)
+ end
+ if vim.fn.isdirectory(histdir) == 1 and type(opts.__resume_key) == "string" then
+ opts.fzf_opts["--history"] = path.join({ histdir, opts.__resume_key })
+ end
+ end
+
-- prioritize fzf-tmux split pane flags over the
-- popup flag `-p` from fzf-lua defaults (#865)
+ opts._is_fzf_tmux_popup = true
if type(opts.fzf_tmux_opts) == "table" then
for _, flag in ipairs({ "-u", "-d", "-l", "-r" }) do
if opts.fzf_tmux_opts[flag] then
+ opts._is_fzf_tmux_popup = false
opts.fzf_tmux_opts["-p"] = nil
end
end
@@ -299,7 +268,13 @@ function M.normalize_opts(opts, globals, __resume_key)
-- also check if we need to override 'opts.prompt' from cli args
-- if we don't override 'opts.prompt' 'FzfWin.save_query' will
-- fail to remove the prompt part from resume saved query (#434)
- for _, s in ipairs({ "fzf_args", "fzf_cli_args", "fzf_raw_args" }) do
+ for _, s in ipairs({
+ "fzf_args",
+ "fzf_cli_args",
+ "fzf_raw_args",
+ "file_icon_padding",
+ "dir_icon",
+ }) do
if opts[s] == nil then
opts[s] = M.globals[s]
end
@@ -383,16 +358,6 @@ function M.normalize_opts(opts, globals, __resume_key)
-- backward compat copy due to the migration of `winopts.hl` -> `hls`
opts.hls = vim.tbl_deep_extend("keep", opts.hls or {}, M.globals.__HLS)
- -- Cache provider specific highlights so we don't call vim functions
- -- within a "fast event" (`vim.in_fast_event()`) and err with:
- -- E5560: vimL function must not be called in a lua loop callback
- for _, hl_opt in ipairs(opts._cached_hls or {}) do
- local hlgroup = opts.hls[hl_opt]
- assert(hlgroup ~= nil) -- must exist
- local _, escseq = utils.ansi_from_hl(hlgroup)
- utils.cache_ansi_escseq(hlgroup, escseq)
- end
-
if type(opts.previewer) == "function" then
-- we use a function so the user can override
-- globals.winopts.preview.default
@@ -412,21 +377,26 @@ function M.normalize_opts(opts, globals, __resume_key)
end
if opts.cwd and #opts.cwd > 0 then
+ -- NOTE: on Windows, `expand` will replace all backslashes with forward slashes
+ -- i.e. C:/Users -> c:\Users
opts.cwd = vim.fn.expand(opts.cwd)
if not vim.loop.fs_stat(opts.cwd) then
utils.warn(("Unable to access '%s', removing 'cwd' option."):format(opts.cwd))
opts.cwd = nil
else
- -- relative paths in cwd are inaccessible when using multiprocess
- -- as the external process have no awareness of our current working
- -- directory so we must convert to full path (#375)
- if not path.starts_with_separator(opts.cwd) then
+ if not path.is_absolute(opts.cwd) then
+ -- relative paths in cwd are inaccessible when using multiprocess
+ -- as the external process have no awareness of our current working
+ -- directory so we must convert to full path (#375)
opts.cwd = path.join({ vim.loop.cwd(), opts.cwd })
+ elseif utils.__IS_WINDOWS and opts.cwd:sub(2) == ":" then
+ -- TODO: upstream bug? on Windoows: starting jobs with `cwd = C:` (without separator)
+ -- ignores the cwd argument and starts the job in the current working directory
+ opts.cwd = path.add_trailing(opts.cwd)
end
end
end
-
-- test for valid git_repo
opts.git_icons = opts.git_icons and path.is_git_repo(opts, true)
@@ -480,20 +450,48 @@ function M.normalize_opts(opts, globals, __resume_key)
end
end
- -- are we using fzf-tmux
+ -- are we using fzf-tmux, if so get available columns
opts._is_fzf_tmux = vim.env.TMUX and opts.fzf_bin:match("fzf%-tmux$")
+ if opts._is_fzf_tmux then
+ local out = utils.io_system({ "tmux", "display-message", "-p", "#{window_width}" })
+ opts._tmux_columns = tonumber(out:match("%d+"))
+ opts.winopts.split = nil
+ end
+
+ -- refresh highlights if background/colorscheme changed (#1092)
+ if not M.__HLS_STATE
+ or M.__HLS_STATE.bg ~= vim.o.bg
+ or M.__HLS_STATE.colorscheme ~= vim.g.colors_name then
+ utils.setup_highlights()
+ end
+
+ -- Cache provider specific highlights so we don't call vim functions
+ -- within a "fast event" (`vim.in_fast_event()`) and err with:
+ -- E5560: vimL function must not be called in a lua loop callback
+ for _, hl_opt in ipairs(opts._cached_hls or {}) do
+ local hlgroup = opts.hls[hl_opt]
+ assert(hlgroup ~= nil) -- must exist
+ local _, escseq = utils.ansi_from_hl(hlgroup)
+ utils.cache_ansi_escseq(hlgroup, escseq)
+ end
+
+
+ if devicons.plugin_loaded() then
+ -- refresh icons, does nothing if "vim.o.bg" didn't change
+ devicons.load({
+ icon_padding = opts.file_icon_padding,
+ dir_icon = { icon = opts.dir_icon, color = utils.hexcol_from_hl(opts.hls.dir_icon, "fg") }
+ })
+ elseif opts.file_icons then
+ -- Disable devicons if not available
+ utils.warn("nvim-web-devicons isn't available, disabling 'file_icons'.")
+ opts.file_icons = nil
+ end
-- libuv.spawn_nvim_fzf_cmd() pid callback
opts._set_pid = M.set_pid
opts._get_pid = M.get_pid
- -- setup devicons terminal highlight groups does nothing unless
- -- neovim `bg` is changed, call via utils/loadstring to prevent
- -- circular require and also make sure "make_entry.lua" isn't
- -- loaded before devicons vars are setup by this module
- -- TODO: cleanup the background update and devicons load logic
- utils.setup_devicon_term_hls()
-
-- mark as normalized
opts._normalized = true
@@ -545,7 +543,6 @@ M._action_to_helpstr = {
[actions.buf_del] = "buffer-delete",
[actions.buf_switch] = "buffer-switch",
[actions.buf_switch_or_edit] = "buffer-switch-or-edit",
- [actions.colorscheme] = "set-colorscheme",
[actions.run_builtin] = "run-builtin",
[actions.ex_run] = "edit-cmd",
[actions.ex_run_cr] = "exec-cmd",
@@ -568,6 +565,8 @@ M._action_to_helpstr = {
[actions.man] = "man-open",
[actions.man_vert] = "man-vertical",
[actions.man_tab] = "man-tab",
+ [actions.git_branch_add] = "git-branch-add",
+ [actions.git_branch_del] = "git-branch-del",
[actions.git_switch] = "git-switch",
[actions.git_checkout] = "git-checkout",
[actions.git_reset] = "git-reset",
@@ -592,6 +591,11 @@ M._action_to_helpstr = {
[actions.set_qflist] = "set-{qf|loc}list",
[actions.apply_profile] = "apply-profile",
[actions.complete] = "complete",
+ [actions.dap_bp_del] = "dap-bp-delete",
+ [actions.colorscheme] = "colorscheme-apply",
+ [actions.cs_delete] = "colorscheme-delete",
+ [actions.cs_update] = "colorscheme-update",
+ [actions.toggle_bg] = "toggle-background",
}
return M
diff --git a/lua/fzf-lua/core.lua b/lua/fzf-lua/core.lua
index 36015a83..59f8dd49 100644
--- a/lua/fzf-lua/core.lua
+++ b/lua/fzf-lua/core.lua
@@ -6,24 +6,63 @@ local actions = require "fzf-lua.actions"
local win = require "fzf-lua.win"
local libuv = require "fzf-lua.libuv"
local shell = require "fzf-lua.shell"
+local devicons = require "fzf-lua.devicons"
local make_entry = require "fzf-lua.make_entry"
+local base64 = require "fzf-lua.lib.base64"
+local serpent = require "fzf-lua.lib.serpent"
local M = {}
M.ACTION_DEFINITIONS = {
-- list of supported actions with labels to be displayed in the headers
-- no pos implies an append to header array
- [actions.toggle_ignore] = { "Disable .gitignore", fn_reload = "Respect .gitignore" },
- [actions.grep_lgrep] = { "Regex Search", fn_reload = "Fuzzy Search" },
- [actions.sym_lsym] = { "Live Query", fn_reload = "Fuzzy Search" },
+ [actions.toggle_ignore] = {
+ function(o)
+ local flag = o.toggle_ignore_flag or "--no-ignore"
+ if o.cmd:match(utils.lua_regex_escape(flag)) then
+ return "Respect .gitignore"
+ else
+ return "Disable .gitignore"
+ end
+ end,
+ },
+ [actions.grep_lgrep] = {
+ function(o)
+ if o.fn_reload then
+ return "Fuzzy Search"
+ else
+ return "Regex Search"
+ end
+ end,
+ },
+ [actions.sym_lsym] = {
+ function(o)
+ if o.fn_reload then
+ return "Fuzzy Search"
+ else
+ return "Live Query"
+ end
+ end,
+ },
+ [actions.toggle_bg] = {
+ function(_)
+ -- return string.format("set bg=%s", vim.o.background == "dark" and "light" or "dark")
+ return "toggle bg"
+ end,
+ },
[actions.buf_del] = { "close" },
[actions.arg_del] = { "delete" },
+ [actions.dap_bp_del] = { "delete" },
+ [actions.cs_delete] = { "uninstall" },
+ [actions.cs_update] = { "[down|re]-load" },
[actions.git_reset] = { "reset" },
[actions.git_stage] = { "stage", pos = 1 },
[actions.git_unstage] = { "unstage", pos = 2 },
[actions.git_stage_unstage] = { "[un-]stage", pos = 1 },
[actions.git_stash_drop] = { "drop a stash" },
[actions.git_yank_commit] = { "copy commit hash" },
+ [actions.git_branch_add] = { "add branch" },
+ [actions.git_branch_del] = { "delete branch" },
}
-- converts contents array sent to `fzf_exec` into a single contents
@@ -76,8 +115,12 @@ local contents_from_arr = function(cont_arr)
return contents
end
+---@alias content table|function|string|nil
+
-- Main API, see:
-- https://github.com/ibhagwan/fzf-lua/wiki/Advanced
+---@param contents content
+---@param opts {fn_reload: string|function, fn_transform: function, __fzf_init_cmd: string, _normalized: boolean}
M.fzf_exec = function(contents, opts)
if type(contents) == "table" and type(contents[1]) == "table" then
contents = contents_from_arr(contents)
@@ -89,9 +132,9 @@ M.fzf_exec = function(contents, opts)
-- save a copy of cprovider info in the opts, we later use it for better named
-- quickfix lists, use `pcall` because we will circular ref main object (#776)
_, opts.__INFO = pcall(loadstring("return require'fzf-lua'.get_info()"))
- opts.fn_selected = opts.fn_selected or function(selected)
+ opts.fn_selected = opts.fn_selected or function(selected, o)
if not selected then return end
- actions.act(opts.actions, selected, opts)
+ actions.act(opts.actions, selected, o)
end
-- wrapper for command transformer
if type(contents) == "string" and (opts.fn_transform or opts.fn_preprocess) then
@@ -117,7 +160,7 @@ M.fzf_exec = function(contents, opts)
-- the caller requested to transform, we need to convert
-- to a function that returns string so that libuv.spawn
-- is called
- local cmd = opts.fn_reload
+ local cmd = opts.fn_reload --[[@as string]]
opts.fn_reload = function(q)
if cmd:match(M.fzf_query_placeholder) then
return cmd:gsub(M.fzf_query_placeholder, q or "")
@@ -156,6 +199,10 @@ M.fzf_resume = function(opts)
M.fzf_exec(config.__resume_data.contents, opts)
end
+---@param opts table
+---@param contents content
+---@param fn_selected function?
+---@return function
M.fzf_wrap = function(opts, contents, fn_selected)
opts = opts or {}
return coroutine.wrap(function()
@@ -167,6 +214,11 @@ M.fzf_wrap = function(opts, contents, fn_selected)
xpcall(function()
opts.fn_selected(selected, opts)
end, function(err)
+ -- ignore existing swap file error, the choices dialog will still be
+ -- displayed to user to make a selection once fzf-lua exits (#1011)
+ if err:match("Vim%(edit%):E325") then
+ return
+ end
utils.err("fn_selected threw an error: " .. debug.traceback(err, 1))
end)
end
@@ -211,6 +263,9 @@ M.CTX = function(includeBuflist)
return M.__CTX
end
+---@param contents content
+---@param opts table?
+---@return string[]?
M.fzf = function(contents, opts)
-- Disable opening from the command-line window `:q`
-- creates all kinds of issues, will fail on `nvim_win_close`
@@ -228,7 +283,7 @@ M.fzf = function(contents, opts)
-- this provides a solution for saving the query
-- when the user pressed a valid bind but not when
-- aborting with or , see next comment
- opts.fzf_opts["--print-query"] = ""
+ opts.fzf_opts["--print-query"] = true
-- setup dummy callbacks for the default fzf 'abort' keybinds
-- this way the query also gets saved when we do not 'accept'
opts.actions = opts.actions or {}
@@ -252,8 +307,6 @@ M.fzf = function(contents, opts)
config.__resume_data = config.__resume_data or {}
config.__resume_data.opts = utils.deepcopy(opts)
config.__resume_data.contents = contents and utils.deepcopy(contents) or nil
- -- save a ref to resume data for 'grep_lgrep'
- opts.__resume_data = config.__resume_data
end
-- update context and save a copy in options (for actions)
-- call before creating the window or fzf_winobj is not nil
@@ -287,9 +340,29 @@ M.fzf = function(contents, opts)
-- fzf 0.40 added 'zero' event for when there's no match
-- clears the preview when there are no matching entries
if opts.__FZF_VERSION and opts.__FZF_VERSION >= 0.40 and previewer.zero then
- opts.keymap = opts.keymap or {}
- opts.keymap.fzf = opts.keymap.fzf or {}
- opts.keymap.fzf["zero"] = previewer:zero()
+ utils.map_set(opts, "keymap.fzf.zero", previewer:zero())
+ end
+ if opts.__FZF_VERSION
+ and opts.__FZF_VERSION >= 0.46
+ and opts.winopts.preview.layout == "flex"
+ and tonumber(opts.winopts.preview.flip_columns) > 0
+ -- Only enable flex layout native rotate if zero event wasn't
+ -- set as it's most likely set by the default builtin previewer
+ and (not previewer.zero
+ -- or when using split mode for the background "empty previewer"
+ -- do not use when starting with a hidden previewer as this will
+ -- display the empty previewer when resizing (#1130)
+ or opts.winopts.split and opts.winopts.preview.hidden ~= "hidden")
+ then
+ local transformer = string.format(utils.__IS_WINDOWS
+ and "IF %%FZF_COLUMNS%% LEQ %d (echo change-preview-window:%s) "
+ .. "ELSE (echo change-preview-window:%s)"
+ or "[ $FZF_COLUMNS -le %d ] && echo change-preview-window:%s "
+ .. "|| echo change-preview-window:%s",
+ tonumber(opts.winopts.preview.flip_columns),
+ opts.winopts.preview.vertical,
+ opts.winopts.preview.horizontal)
+ utils.map_set(opts, "keymap.fzf.resize", string.format("transform(%s)", transformer))
end
if type(previewer.preview_window) == "function" then
-- do we need to override the preview_window args?
@@ -305,8 +378,8 @@ M.fzf = function(contents, opts)
if type(previewer.fzf_delimiter) == "function" then
opts.fzf_opts["--delimiter"] = previewer:fzf_delimiter()
end
- if type(previewer.preview_offset) == "function" then
- opts.preview_offset = previewer:preview_offset()
+ if opts.preview_offset == nil and type(previewer._preview_offset) == "function" then
+ opts.preview_offset = previewer:_preview_offset()
end
elseif not opts.preview and not opts.fzf_opts["--preview"] then
-- no preview available, override in case $FZF_DEFAULT_OPTS
@@ -376,18 +449,13 @@ M.fzf = function(contents, opts)
return selected
end
-
+---@param o table
+---@return string
M.preview_window = function(o)
- local preview_args = ("%s:%s:%s:"):format(
- o.winopts.preview.hidden, o.winopts.preview.border, o.winopts.preview.wrap)
- if o.winopts.preview.layout == "horizontal" or
- o.winopts.preview.layout == "flex" and
- vim.o.columns > o.winopts.preview.flip_columns then
- preview_args = preview_args .. o.winopts.preview.horizontal
- else
- preview_args = preview_args .. o.winopts.preview.vertical
- end
- return preview_args
+ local hsplit = win:preview_splits_horizontally(o.winopts, 0)
+ local split = hsplit and o.winopts.preview.horizontal or o.winopts.preview.vertical
+ return ("%s:%s:%s:%s"):format(
+ o.winopts.preview.hidden, o.winopts.preview.border, o.winopts.preview.wrap, split)
end
-- Create fzf --color arguments from a table of vim highlight groups.
@@ -396,23 +464,39 @@ M.create_fzf_colors = function(opts)
if type(colors) == "function" then
colors = colors(opts)
end
- if not colors then return end
local tbl = {}
- for highlight, list in pairs(colors) do
+
+ -- In case the user alredy set fzf_opts["--color"] (#1052)
+ table.insert(tbl, opts.fzf_opts and opts.fzf_opts["--color"])
+
+ for flag, list in pairs(colors or {}) do
if type(list) == "table" then
- local hexcol = utils.hexcol_from_hl(list[2], list[1])
- if hexcol and #hexcol > 0 then
- table.insert(tbl, ("%s:%s"):format(highlight, hexcol))
+ local spec = {}
+ local what = list[1]
+ -- [2] can be one or more highlights, first existing hl wins
+ local hls = type(list[2]) == "table" and list[2] or { list[2] }
+ for _, hl in ipairs(hls) do
+ local hexcol = utils.hexcol_from_hl(hl, what)
+ if hexcol and #hexcol > 0 then
+ table.insert(spec, hexcol)
+ break
+ end
end
-- arguments in the 3rd slot onward are passed raw, this can
-- be used to pass styling arguments, for more info see #413
-- https://github.com/junegunn/fzf/issues/1663
for i = 3, #list do
- table.insert(tbl, ("%s:%s"):format(highlight, list[i]))
+ if type(list[i]) == "string" then
+ table.insert(spec, list[i])
+ end
+ end
+ if not vim.tbl_isempty(spec) then
+ table.insert(spec, 1, flag)
+ table.insert(tbl, table.concat(spec, ":"))
end
elseif type(list) == "string" then
- table.insert(tbl, ("%s:%s"):format(highlight, list))
+ table.insert(tbl, ("%s:%s"):format(flag, list))
end
end
@@ -428,20 +512,18 @@ M.create_fzf_binds = function(binds)
if type(v) == "table" then
v = v[1]
end
- -- backward compatibility to when binds
- -- where defined as one string ':'
if v then
- local key, action = v:match("(.*):(.*)")
- if action then k, v = key, action end
dedup[k] = v
end
end
for key, action in pairs(dedup) do
table.insert(tbl, string.format("%s:%s", key, action))
end
- return vim.fn.shellescape(table.concat(tbl, ","))
+ return table.concat(tbl, ",")
end
+---@param opts table
+---@return string[]
M.build_fzf_cli = function(opts)
opts.fzf_opts = vim.tbl_extend("force", config.globals.fzf_opts, opts.fzf_opts or {})
-- copy from globals
@@ -453,114 +535,100 @@ M.build_fzf_cli = function(opts)
}) do
opts[o] = opts[o] or config.globals[o]
end
- -- preview and query have special handling:
- -- 'opts.' is prioritized over 'fzf_opts[--name]'
- -- 'opts.' is automatically shellescaped
- for _, o in ipairs({ "query", "preview" }) do
- local flag = string.format("--%s", o)
- if opts[o] ~= nil then
- -- opt can be 'false' (disabled)
- -- don't shellescape in this case
- opts.fzf_opts[flag] = opts[o] and libuv.shellescape(opts[o])
- else
- opts.fzf_opts[flag] = opts.fzf_opts[flag]
+ -- below options can be specified directly in opts and will be
+ -- prioritized: opts. is prioritized over fzf_opts["--name"]
+ for _, flag in ipairs({ "query", "prompt", "header", "preview" }) do
+ if opts[flag] ~= nil then
+ opts.fzf_opts["--" .. flag] = opts[flag]
end
end
opts.fzf_opts["--bind"] = M.create_fzf_binds(opts.keymap.fzf)
- if opts.fzf_colors then
- opts.fzf_opts["--color"] = M.create_fzf_colors(opts)
- end
+ opts.fzf_opts["--color"] = M.create_fzf_colors(opts)
opts.fzf_opts["--expect"] = actions.expect(opts.actions)
if opts.fzf_opts["--preview-window"] == nil then
opts.fzf_opts["--preview-window"] = M.preview_window(opts)
end
- if opts.preview_offset and #opts.preview_offset > 0 then
+ if opts.fzf_opts["--preview-window"] and opts.preview_offset and #opts.preview_offset > 0 then
opts.fzf_opts["--preview-window"] =
opts.fzf_opts["--preview-window"] .. ":" .. opts.preview_offset
end
- -- shell escape the prompt
- opts.fzf_opts["--prompt"] = (opts.prompt or opts.fzf_opts["--prompt"]) and
- vim.fn.shellescape(opts.prompt or opts.fzf_opts["--prompt"])
- -- multi | no-multi (select)
- if opts.nomulti or opts.fzf_opts["--no-multi"] then
- opts.fzf_opts["--multi"] = nil
- opts.fzf_opts["--no-multi"] = ""
- else
- opts.fzf_opts["--multi"] = ""
- opts.fzf_opts["--no-multi"] = nil
- end
- -- backward compatibility, add all previously known options
- for k, v in pairs({
- ["--ansi"] = "fzf_ansi",
- ["--layout"] = "fzf_layout"
- }) do
- if opts[v] and #opts[v] == 0 then
- opts.fzf_opts[k] = nil
- elseif opts[v] then
- opts.fzf_opts[k] = opts[v]
+ if opts.__FZF_VERSION and opts.__FZF_VERSION < 0.42 then
+ if opts.fzf_opts["--info"]:match("^inline%-right") then
+ opts.fzf_opts["--info"] = "inline"
end
end
- local extra_args = ""
- for _, o in ipairs({
- "fzf_args",
- "fzf_raw_args",
- "fzf_cli_args",
- "_fzf_cli_args",
- }) do
- if opts[o] then extra_args = extra_args .. " " .. opts[o] end
- end
if opts._is_skim then
+ -- skim (rust version of fzf) doesn't support the '--info=' flag
local info = opts.fzf_opts["--info"]
- -- skim (rust version of fzf) doesn't
- -- support the '--info=' flag
opts.fzf_opts["--info"] = nil
- if info == "inline" then
+ if info:match("^inline") then
-- inline for skim is defined as:
- opts.fzf_opts["--inline-info"] = ""
+ opts.fzf_opts["--inline-info"] = true
end
-- skim doesn't accept border args
- local border = opts.fzf_opts["--border"]
- if border == "none" then
+ if opts.fzf_opts["--border"] == "none" then
opts.fzf_opts["--border"] = nil
else
- opts.fzf_opts["--border"] = ""
+ opts.fzf_opts["--border"] = true
end
end
- -- build the clip args
- local cli_args = ""
+ -- build the cli args
+ local cli_args = {}
-- fzf-tmux args must be included first
if opts._is_fzf_tmux then
for k, v in pairs(opts.fzf_tmux_opts or {}) do
- if v then cli_args = cli_args .. string.format(" %s %s", k, v) end
+ table.insert(cli_args, k)
+ if type(v) == "string" and #v > 0 then
+ table.insert(cli_args, v)
+ end
end
end
for k, v in pairs(opts.fzf_opts) do
- if type(v) == "table" then
- -- table argument is meaningless here
- v = nil
- elseif type(v) == "number" then
- -- convert to string
- v = string.format("%d", v)
- end
+ -- flag can be set to `false` to negate a default
if v then
- v = v:gsub(k .. "=", "")
- cli_args = cli_args ..
- (" %s%s"):format(k, #v > 0 and "=" .. v or "")
+ local opt_v
+ if type(v) == "string" or type(v) == "number" then
+ v = tostring(v) -- convert number type to string
+ if k == "--query" then
+ opt_v = libuv.shellescape(v)
+ else
+ if utils.__IS_WINDOWS and type(v) == "string" and v:match([[^'.*'$]]) then
+ -- replace single quote shellescape
+ -- TODO: replace all so we never get here
+ v = [["]] .. v:sub(2, #v - 1) .. [["]]
+ end
+ if libuv.is_escaped(v) then
+ utils.warn(string.format("`fzf_opts` are automatically shellescaped."
+ .. " Please remove surrounding quotes from %s=%s", k, v))
+ end
+ opt_v = libuv.is_escaped(v) and v or libuv.shellescape(v)
+ end
+ end
+ if opts._is_skim then
+ -- skim has a bug with flag values that start with `-`, for example
+ -- specifying `--nth "-1.."` will fail but `--nth="-1.."` works (#1085)
+ table.insert(cli_args, not opt_v and k or string.format("%s=%s", k, opt_v))
+ else
+ table.insert(cli_args, k)
+ if opt_v then table.insert(cli_args, opt_v) end
+ end
end
end
- return cli_args .. extra_args
+ for _, o in ipairs({ "fzf_args", "fzf_raw_args", "fzf_cli_args", "_fzf_cli_args" }) do
+ if opts[o] then
+ table.insert(cli_args, type(opts[o]) == "table" and opts[o] or tostring(opts[o]))
+ end
+ end
+ return cli_args
end
+---@param opts table
+---@return string|function
M.mt_cmd_wrapper = function(opts)
assert(opts and opts.cmd)
-
- local str_to_str = function(s)
- -- use long format of bracket escape so we can include "]" (#925)
- -- https://www.lua.org/manual/5.4/manual.html#3.1
- return "[==[" .. s .. "]==]"
- end
-
- local opts_to_str = function(o)
+ ---@param o table
+ ---@return table
+ local filter_opts = function(o)
local names = {
"debug",
"argv_expr",
@@ -576,30 +644,47 @@ M.mt_cmd_wrapper = function(opts)
"color_icons",
"path_shorten",
"strip_cwd_prefix",
+ "exec_empty_query",
"file_ignore_patterns",
"rg_glob",
- "__module__",
+ "_base64",
}
-- caller reqested rg with glob support
if o.rg_glob then
table.insert(names, "glob_flag")
table.insert(names, "glob_separator")
end
- local str = ""
+ local t = {}
for _, name in ipairs(names) do
if o[name] ~= nil then
- if #str > 0 then str = str .. "," end
- local val = o[name]
- if type(val) == "string" then
- val = str_to_str(val)
- end
- if type(val) == "table" then
- val = vim.inspect(val)
- end
- str = str .. ("%s=%s"):format(name, val)
+ t[name] = o[name]
end
end
- return "{" .. str .. "}"
+ t.g = {}
+ for k, v in pairs({
+ ["_fzf_lua_server"] = vim.g.fzf_lua_server,
+ ["_devicons_path"] = devicons.plugin_path(),
+ ["_devicons_setup"] = config._devicons_setup,
+ }) do
+ t.g[k] = v
+ end
+ return t
+ end
+
+ ---@param obj table|string
+ ---@return string
+ local serialize = function(obj)
+ local str = type(obj) == "table"
+ and serpent.line(obj, { comment = false, sortkeys = false })
+ or tostring(obj)
+ if opts._base64 ~= false then
+ -- by default, base64 encode all arguments
+ return "[==[" .. base64.encode(str) .. "]==]"
+ else
+ -- if not encoding, don't string wrap the table
+ return type(obj) == "table" and str
+ or "[==[" .. str .. "]==]"
+ end
end
if not opts.requires_processing
@@ -607,35 +692,32 @@ M.mt_cmd_wrapper = function(opts)
and not opts.file_icons
and not opts.file_ignore_patterns
and not opts.path_shorten then
- -- command does not require any processing
+ -- command does not require any processing, we also reset `argv_expr`
+ -- to keep `setup_fzf_interactive_flags::no_query_condi` in the command
+ opts.argv_expr = nil
return opts.cmd
elseif opts.multiprocess then
assert(not opts.__mt_transform or type(opts.__mt_transform) == "string")
assert(not opts.__mt_preprocess or type(opts.__mt_preprocess) == "string")
- local fn_preprocess = opts.__mt_preprocess or [[return require("make_entry").preprocess]]
- local fn_transform = opts.__mt_transform or [[return require("make_entry").file]]
- -- replace all below 'fn.shellescape' with our version
- -- replacing the surrounding single quotes with double
- -- as this was causing resume to fail with fish shell
- -- due to fzf replacing ' with \ (no idea why)
- if not opts.no_remote_config then
- fn_transform = ([[_G._fzf_lua_server=%s; %s]]):format(
- libuv.shellescape(vim.g.fzf_lua_server),
- fn_transform)
- end
- if config._devicons_setup then
- fn_transform = ([[_G._devicons_setup=%s; %s]]):format(
- libuv.shellescape(config._devicons_setup),
- fn_transform)
- end
- if config._devicons_path then
- fn_transform = ([[_G._devicons_path=%s; %s]]):format(
- libuv.shellescape(config._devicons_path),
- fn_transform)
- end
- local cmd = libuv.wrap_spawn_stdio(opts_to_str(opts), fn_transform, fn_preprocess)
- if opts.debug_cmd or opts.debug and not (opts.debug_cmd == false) then
- utils.info(string.format("multiprocess cmd: %s", cmd))
+ if opts.argv_expr then
+ -- Since the `rg` command will be wrapped inside the shell escaped
+ -- '--headless .. --cmd', we won't be able to search single quotes
+ -- as it will break the escape sequence. So we use a nifty trick:
+ -- * replace the placeholder with {argv1}
+ -- * re-add the placeholder at the end of the command
+ -- * preprocess then replace it with vim.fn.argv(1)
+ -- NOTE: since we cannot guarantee the positional index
+ -- of arguments (#291), we use the last argument instead
+ opts.cmd = opts.cmd:gsub(M.fzf_query_placeholder, "{argvz}")
+ end
+ local cmd = libuv.wrap_spawn_stdio(
+ serialize(filter_opts(opts)),
+ serialize(opts.__mt_transform or [[return require("make_entry").file]]),
+ serialize(opts.__mt_preprocess or [[return require("make_entry").preprocess]])
+ )
+ if opts.argv_expr then
+ -- prefix the query with `--` so we can support `--fixed-strings` (#781)
+ cmd = string.format("%s -- %s", cmd, M.fzf_query_placeholder)
end
return cmd
else
@@ -677,10 +759,10 @@ end
M.set_header = function(opts, hdr_tbl)
local function normalize_cwd(cwd)
- if path.starts_with_separator(cwd) and cwd ~= vim.loop.cwd() then
+ if path.is_absolute(cwd) and not path.equals(cwd, vim.loop.cwd()) then
-- since we're always converting cwd to full path
-- try to convert it back to relative for display
- cwd = path.relative(cwd, vim.loop.cwd())
+ cwd = path.relative_to(cwd, vim.loop.cwd())
end
-- make our home dir path look pretty
return path.HOME_to_tilde(cwd)
@@ -693,9 +775,7 @@ M.set_header = function(opts, hdr_tbl)
#opts.prompt >= tonumber(opts.cwd_prompt_shorten_len) then
opts.prompt = path.shorten(opts.prompt, tonumber(opts.cwd_prompt_shorten_val) or 1)
end
- if not path.ends_with_separator(opts.prompt) then
- opts.prompt = opts.prompt .. path.SEPARATOR
- end
+ opts.prompt = path.add_trailing(opts.prompt)
end
if opts.no_header or opts.headers == false then
return opts
@@ -713,7 +793,8 @@ M.set_header = function(opts, hdr_tbl)
-- cwd unless the caller specifically requested
if opts.cwd_header == false or
opts.cwd_prompt and opts.cwd_header == nil or
- opts.cwd_header == nil and (not opts.cwd or opts.cwd == vim.loop.cwd()) then
+ opts.cwd_header == nil and
+ (not opts.cwd or path.equals(opts.cwd, vim.loop.cwd())) then
return
end
return normalize_cwd(opts.cwd or vim.loop.cwd())
@@ -746,7 +827,7 @@ M.set_header = function(opts, hdr_tbl)
actions = {
hdr_txt_opt = "interactive_header_txt",
hdr_txt_str = "",
- val = function()
+ val = function(o)
if opts.no_header_i then return end
local defs = M.ACTION_DEFINITIONS
local ret = {}
@@ -754,7 +835,10 @@ M.set_header = function(opts, hdr_tbl)
local action = type(v) == "function" and v or type(v) == "table" and (v.fn or v[1])
if type(action) == "function" and defs[action] then
local def = defs[action]
- local to = (opts.fn_reload or opts._hdr_to) and def.fn_reload or def[1]
+ local to = def[1]
+ if type(to) == "function" then
+ to = to(o)
+ end
table.insert(ret, def.pos or #ret + 1,
string.format("<%s> to %s",
utils.ansi_from_hl(opts.hls.header_bind, k),
@@ -791,7 +875,7 @@ M.set_header = function(opts, hdr_tbl)
for _, h in ipairs(opts.headers) do
assert(definitions[h])
local def = definitions[h]
- local txt = def.val()
+ local txt = def.val(opts)
if def and txt then
hdr_str = not hdr_str and "" or (hdr_str .. ", ")
hdr_str = ("%s%s%s"):format(hdr_str, def.hdr_txt_str,
@@ -800,16 +884,19 @@ M.set_header = function(opts, hdr_tbl)
end
end
if hdr_str and #hdr_str > 0 then
- opts.fzf_opts["--header"] = libuv.shellescape(hdr_str)
+ opts.fzf_opts["--header"] = hdr_str
end
return opts
end
-- converts actions defined with "reload=true" to use fzf's `reload` bind
-- provides a better UI experience without a visible interface refresh
+---@param reload_cmd content
+---@param opts table
+---@return table
M.convert_reload_actions = function(reload_cmd, opts)
- local fallback
- local has_reload
+ local fallback ---@type boolean?
+ local has_reload ---@type boolean?
if opts._is_skim or type(reload_cmd) ~= "string" then
fallback = true
end
@@ -892,6 +979,8 @@ end
-- converts actions defined inside 'silent_actions' to use fzf's 'execute-silent'
-- bind, these actions will not close the UI, e.g. commits|bcommits yank commit sha
+---@param opts table
+---@return table
M.convert_exec_silent_actions = function(opts)
if opts._is_skim then
return opts
@@ -916,6 +1005,10 @@ M.convert_exec_silent_actions = function(opts)
return opts
end
+---@param command string
+---@param fzf_field_expression string
+---@param opts table
+---@return table
M.setup_fzf_interactive_flags = function(command, fzf_field_expression, opts)
-- query cannot be 'nil'
opts.query = opts.query or ""
@@ -924,8 +1017,7 @@ M.setup_fzf_interactive_flags = function(command, fzf_field_expression, opts)
-- we make sure a clear error message is displayed
-- when the user enters bad regex expressions
local initial_command = command
- if (opts.stderr_to_stdout ~= false) and
- not initial_command:match("2>") then
+ if (opts.stderr_to_stdout ~= false) and not initial_command:match("2>") then
initial_command = command .. " 2>&1"
end
@@ -933,16 +1025,37 @@ M.setup_fzf_interactive_flags = function(command, fzf_field_expression, opts)
if type(opts.query_delay) == "number" then
reload_command = string.format("sleep %.2f; %s", opts.query_delay / 1000, reload_command)
end
- if not opts.exec_empty_query then
- reload_command = ("[ -z %s ] || %s"):format(fzf_field_expression, reload_command)
- end
+ -- See the note in `make_entry.preprocess`, the NEQ condition on Windows
+ -- along with fzf's lacking escape sequence causes the empty query condition
+ -- to fail on spaces, comma and semicolon (and perhaps other use cases),
+ -- moving the test to our cmd wrapper solves it for anything but "native"
+ local no_query_condi = (opts.exec_empty_query or opts.argv_expr) and ""
+ or string.format(
+ utils._if_win(
+ -- due to the reload command already being shellescaped and fzf's {q}
+ -- also escaping the query with ^""^ any spaces in the query
+ -- will fail the command, by adding caret escaping before fzf's
+ -- we fool CMD.exe to not terminate the quote and thus an empty query
+ -- will generate the experssion ^^"^" which translates to ^""
+ -- our specialized libuv.shellescape will also double the escape
+ -- sequence if a "!" is found in our string as explained in:
+ -- https://ss64.com/nt/syntax-esc.html
+ -- TODO: open an upstream bug rgd ! as without the double escape
+ -- if an ! is found in the command (i.e. -g "rg ... -g !.git")
+ -- sending a caret will require doubling (i.e. sending ^^ for ^)
+ [[IF ^%s NEQ ^^"^" ]],
+ "[ -z %s ] || "),
+ -- {q} for fzf is automatically shell escaped
+ fzf_field_expression
+ )
+
if opts._is_skim then
-- skim interactive mode does not need a piped command
opts.__fzf_init_cmd = nil
opts.prompt = opts.__prompt or opts.prompt or opts.fzf_opts["--prompt"]
if opts.prompt then
opts.fzf_opts["--prompt"] = opts.prompt:match("[^%*]+")
- opts.fzf_opts["--cmd-prompt"] = libuv.shellescape(opts.prompt)
+ opts.fzf_opts["--cmd-prompt"] = opts.prompt
-- save original prompt and reset the current one since
-- we're using the '--cmd-prompt' as the "main" prompt
-- required for resume to have the asterisk prompt prefix
@@ -951,7 +1064,7 @@ M.setup_fzf_interactive_flags = function(command, fzf_field_expression, opts)
end
-- since we surrounded the skim placeholder with quotes
-- we need to escape them in the initial query
- opts.fzf_opts["--cmd-query"] = libuv.shellescape(utils.sk_escape(opts.query))
+ opts.fzf_opts["--cmd-query"] = utils.sk_escape(opts.query)
-- '--query' was set by 'resume()', skim has the option to switch back and
-- forth between interactive command and fuzzy matching (using 'ctrl-q')
-- setting both '--query' and '--cmd-query' will use to fuzzy match
@@ -960,25 +1073,29 @@ M.setup_fzf_interactive_flags = function(command, fzf_field_expression, opts)
opts.query = nil
-- setup as interactive
opts._fzf_cli_args = string.format("--interactive --cmd %s",
- libuv.shellescape(reload_command))
+ libuv.shellescape(no_query_condi .. reload_command))
else
-- **send an empty table to avoid running $FZF_DEFAULT_COMMAND
-- The above seems to create a hang in some systems
-- use `true` as $FZF_DEFAULT_COMMAND instead (#510)
- opts.__fzf_init_cmd = "true"
+ opts.__fzf_init_cmd = utils.shell_nop()
if opts.exec_empty_query or (opts.query and #opts.query > 0) then
- opts.__fzf_init_cmd = initial_command:gsub(fzf_field_expression,
- libuv.shellescape(opts.query:gsub("%%", "%%%%")))
+ -- gsub doesn't like single % on rhs
+ local escaped_q = libuv.shellescape(libuv.escape_fzf(opts.query)):gsub("%%", "%%%%")
+ opts.__fzf_init_cmd = initial_command:gsub(fzf_field_expression, escaped_q)
+ end
+ if opts.__FZF_VERSION >= 0.25 then
+ opts.fzf_opts["--disabled"] = true
+ else
+ opts.fzf_opts["--phony"] = true
end
- opts.fzf_opts["--disabled"] = ""
- opts.fzf_opts["--query"] = libuv.shellescape(opts.query)
+ opts.fzf_opts["--query"] = opts.query
-- OR with true to avoid fzf's "Command failed:" message
if opts.silent_fail ~= false then
- reload_command = ("%s || true"):format(reload_command)
+ reload_command = reload_command .. " || " .. utils.shell_nop()
end
- opts._fzf_cli_args = string.format("--bind=%s",
- libuv.shellescape(("change:reload:%s"):format(
- ("%s"):format(reload_command))))
+ opts._fzf_cli_args = string.format("--bind=%s", libuv.shellescape(
+ string.format("change:reload:%s%s", no_query_condi, reload_command)))
end
return opts
@@ -987,6 +1104,8 @@ end
-- query placeholder for "live" queries
M.fzf_query_placeholder = ""
+---@param opts {_is_skim: boolean}
+---@return string
M.fzf_field_expression = function(opts)
-- fzf already adds single quotes around the placeholder when expanding.
-- for skim we surround it with double quotes or single quote searches fail
diff --git a/lua/fzf-lua/data/colorschemes.json b/lua/fzf-lua/data/colorschemes.json
new file mode 100644
index 00000000..89b36adf
--- /dev/null
+++ b/lua/fzf-lua/data/colorschemes.json
@@ -0,0 +1,647 @@
+{
+ "kanagawa.nvim": {
+ "disp_name": "Kanagawa",
+ "url": "rebelot/kanagawa.nvim",
+ "colorschemes": [
+ "kanagawa",
+ { "name": "kanagawa-wave", "disp_name": "Kanagawa Wave" },
+ { "name": "kanagawa-dragon", "disp_name": "Kanagawa Dragon" },
+ { "name": "kanagawa-lotus", "disp_name": "Kanagawa Lotus" }
+ ]
+ },
+ "tokyonight.nvim": {
+ "disp_name": "Tokyo Night",
+ "url": "folke/tokyonight.nvim",
+ "colorschemes": [
+ "tokyonight",
+ { "name": "tokyonight-night", "disp_name": "Tokyo Night" },
+ { "name": "tokyonight-storm", "disp_name": "Tokyo Storm" },
+ { "name": "tokyonight-day", "disp_name": "Tokyo Day" },
+ { "name": "tokyonight-moon", "disp_name": "Tokyo Moon" }
+ ]
+ },
+ "catppuccin.nvim": {
+ "disp_name": "Catppuccin",
+ "url": "catppuccin/nvim",
+ "colorschemes": [
+ "catppuccin",
+ { "name": "catppuccin-latte", "disp_name": "Catppuccin Latte" },
+ { "name": "catppuccin-frappe", "disp_name": "Catppuccin Frappe" },
+ { "name": "catppuccin-macchiato", "disp_name": "Catppuccin Macchiato" },
+ { "name": "catppuccin-mocha", "disp_name": "Catppuccin Mocha" }
+ ]
+ },
+ "mini.base16": {
+ "disp_name": "Mini Base16",
+ "url": "echasnovski/mini.base16",
+ "colorschemes": [
+ { "name": "minischeme", "disp_name": "Mini Scheme" },
+ { "name": "minicyan", "disp_name": "Mini Cyan" }
+ ]
+ },
+ "vim-nightfly-colors": {
+ "disp_name": "Nightfly",
+ "url": "bluz71/vim-nightfly-colors",
+ "colorschemes": ["nightfly"]
+ },
+ "vim-moonfly-colors": {
+ "disp_name": "Moonfly",
+ "url": "bluz71/vim-moonfly-colors",
+ "colorschemes": ["moonfly"]
+ },
+ "neovim-ayu": {
+ "disp_name": "Neovim Ayu",
+ "url": "Shatur/neovim-ayu",
+ "colorschemes": [
+ {
+ "disp_name": "Ayu Dark",
+ "lua": "require('ayu').setup({mirage = false}); vim.cmd.colorscheme('ayu')"
+ },
+ {
+ "disp_name": "Ayu Mirage",
+ "lua": "require('ayu').setup({mirage = true}); vim.cmd.colorscheme('ayu')"
+ }
+ ]
+ },
+ "rose-pine.nvim": {
+ "disp_name": "Rose Pine",
+ "url": "rose-pine/neovim",
+ "colorschemes": [
+ "rose-pine-main",
+ { "name": "rose-pine-moon", "disp_name": "Rose Pine Moon" },
+ { "name": "rose-pine-dawn", "disp_name": "Rose Pine Dawn" }
+ ]
+ },
+ "everforest-nvim": {
+ "disp_name": "Everforest",
+ "url": "neanias/everforest-nvim",
+ "colorschemes": [
+ {
+ "lua": "require('everforest').setup({background='medium'}); vim.cmd.colorscheme('everforest')"
+ },
+ {
+ "disp_name": "Everforest Hard",
+ "lua": "require('everforest').setup({background='hard'}); vim.cmd.colorscheme('everforest')"
+ },
+ {
+ "disp_name": "Everforest Soft",
+ "lua": "require('everforest').setup({background='soft'}); vim.cmd.colorscheme('everforest')"
+ }
+ ]
+ },
+ "onedark.nvim": {
+ "disp_name": "OneDark.nvim",
+ "url": "navarasu/onedark.nvim",
+ "colorschemes": [
+ {
+ "lua": "require('onedark').setup({style='dark'}); vim.cmd.colorscheme('onedark')"
+ },
+ {
+ "disp_name": "OneDark Darker",
+ "lua": "require('onedark').setup({style='darker'}); vim.cmd.colorscheme('onedark')"
+ },
+ {
+ "disp_name": "OneDark Cool",
+ "lua": "require('onedark').setup({style='cool'}); vim.cmd.colorscheme('onedark')"
+ },
+ {
+ "disp_name": "OneDark Deep",
+ "lua": "require('onedark').setup({style='deep'}); vim.cmd.colorscheme('onedark')"
+ },
+ {
+ "disp_name": "OneDark Warm",
+ "lua": "require('onedark').setup({style='warm'}); vim.cmd.colorscheme('onedark')"
+ },
+ {
+ "disp_name": "OneDark Warmer",
+ "lua": "require('onedark').setup({style='warmer'}); vim.cmd.colorscheme('onedark')"
+ }
+ ]
+ },
+ "onedarkpro.nvim": {
+ "disp_name": "OneDarkPro",
+ "url": "olimorris/onedarkpro.nvim",
+ "colorschemes": [
+ "onedark",
+ { "name": "onelight", "disp_name": "OneDarkPro Light" },
+ { "name": "onedark_vivid", "disp_name": "OneDarkPro Vidid" },
+ { "name": "onedark_dark", "disp_name": "OneDarkPro Dark" }
+ ]
+ },
+ "embark-theme": {
+ "disp_name": "Embark",
+ "url": "embark-theme/vim",
+ "colorschemes": ["embark"]
+ },
+ "nightfox.nvim": {
+ "disp_name": "Nightfox",
+ "url": "EdenEast/nightfox.nvim",
+ "colorschemes": [
+ "nightfox",
+ { "name": "dayfox", "disp_name": "Dayfox" },
+ { "name": "dawnfox", "disp_name": "Dawnfox" },
+ { "name": "duskfox", "disp_name": "Duskfox" },
+ { "name": "nordfox", "disp_name": "Nordfox" },
+ { "name": "terafox", "disp_name": "Terafox" },
+ { "name": "carbonfox", "disp_name": "Carbonfox" }
+ ]
+ },
+ "embark-theme": {
+ "disp_name": "Embark",
+ "url": "embark-theme/vim",
+ "colorschemes": ["embark"]
+ },
+ "github-nvim-theme": {
+ "disp_name": "Github",
+ "url": "projekt0n/github-nvim-theme",
+ "colorschemes": [
+ "github_dark",
+ { "name": "github_dark_default", "disp_name": "Github Dark Default" },
+ { "name": "github_dark_dimmed", "disp_name": "Github Dark Dimmed" },
+ {
+ "name": "github_dark_high_contrast",
+ "disp_name": "Github Dark High Contrast"
+ },
+ {
+ "name": "github_dark_colorblind",
+ "disp_name": "Github Dark Colorblind"
+ },
+ {
+ "name": "github_dark_tritanopia",
+ "disp_name": "Github Dark Tritanopia"
+ },
+ { "name": "github_light_default", "disp_name": "Github Light Default" },
+ {
+ "name": "github_light_high_contrast",
+ "disp_name": "Github Dark High Contrast"
+ },
+ {
+ "name": "github_light_colorblind",
+ "disp_name": "Github Light Colorblind"
+ },
+ {
+ "name": "github_light_tritanopia",
+ "disp_name": "Github Light Tritanopia"
+ }
+ ]
+ },
+ "vscode.nvim": {
+ "disp_name": "VSCode",
+ "url": "Mofiqul/vscode.nvim",
+ "colorschemes": ["vscode"]
+ },
+ "darcula.nvim": {
+ "disp_name": "Dracula",
+ "url": "Mofiqul/dracula.nvim",
+ "colorschemes": ["dracula"]
+ },
+ "oxocarbon.nvim": {
+ "disp_name": "Oxocarbon",
+ "url": "nyoom-engineering/oxocarbon.nvim",
+ "colorschemes": ["oxocarbon"]
+ },
+ "material.nvim": {
+ "disp_name": "Material",
+ "url": "marko-cerovac/material.nvim",
+ "colorschemes": [
+ "material",
+ {
+ "disp_name": "Material Oceanic",
+ "vim": "let g:material_style='oceanic' | colorscheme material"
+ },
+ {
+ "disp_name": "Material Deep Ocean",
+ "lua": "vim.g.material_style='deep ocean'; vim.cmd.colorscheme('material')"
+ },
+ {
+ "disp_name": "Material Pale Night",
+ "lua": "vim.g.material_style='palenight'; vim.cmd.colorscheme('material')"
+ },
+ {
+ "disp_name": "Material Lighter",
+ "lua": "vim.g.material_style='lighter'; vim.cmd.colorscheme('material')"
+ },
+ {
+ "disp_name": "Material Darker",
+ "lua": "vim.g.material_style='darker'; vim.cmd.colorscheme('material')"
+ }
+ ]
+ },
+ "gruvbox-material": {
+ "disp_name": "Gruvbox Material",
+ "url": "sainnhe/gruvbox-material",
+ "colorschemes": ["gruvbox-material"]
+ },
+ "sonokai": {
+ "disp_name": "Sonokai",
+ "url": "sainnhe/sonokai",
+ "colorschemes": [
+ {
+ "lua": "vim.g.sonokai_style='default'; vim.cmd.colorscheme('sonokai')"
+ },
+ {
+ "disp_name": "Sonokai Atlandis",
+ "lua": "vim.g.sonokai_style='atlandis'; vim.cmd.colorscheme('sonokai')"
+ },
+ {
+ "disp_name": "Sonokai Andromeda",
+ "lua": "vim.g.sonokai_style='andromeda'; vim.cmd.colorscheme('sonokai')"
+ },
+ {
+ "disp_name": "Sonokai Shusia",
+ "lua": "vim.g.sonokai_style='shusia'; vim.cmd.colorscheme('sonokai')"
+ },
+ {
+ "disp_name": "Sonokai Maia",
+ "lua": "vim.g.sonokai_style='maia'; vim.cmd.colorscheme('sonokai')"
+ },
+ {
+ "disp_name": "Sonokai Espresso",
+ "lua": "vim.g.sonokai_style='espresso'; vim.cmd.colorscheme('sonokai')"
+ }
+ ]
+ },
+ "edge": {
+ "disp_name": "Edge",
+ "url": "sainnhe/edge",
+ "colorschemes": [
+ {
+ "lua": "vim.g.edge_style='default'; vim.cmd.colorscheme('edge')"
+ },
+ {
+ "disp_name": "Edge Aura",
+ "lua": "vim.g.edge_style='aura'; vim.cmd.colorscheme('edge')"
+ },
+ {
+ "disp_name": "Edge Neon",
+ "lua": "vim.g.edge_style='neon'; vim.cmd.colorscheme('edge')"
+ }
+ ]
+ },
+ "nord.nvim": {
+ "disp_name": "Nord",
+ "url": "shaunsingh/nord.nvim",
+ "colorschemes": ["nord"]
+ },
+ "nordic.nvim": {
+ "disp_name": "Nordic",
+ "url": "AlexvZyl/nordic.nvim",
+ "colorschemes": ["nordic"]
+ },
+ "nordic.nvim.2": {
+ "disp_name": "Nordic",
+ "url": "andersevenrud/nordic.nvim",
+ "colorschemes": ["nordic"]
+ },
+ "solarized-osaka.nvim": {
+ "disp_name": "Solarized Osaka",
+ "url": "craftzdog/solarized-osaka.nvim",
+ "colorschemes": ["solarized-osaka"]
+ },
+ "melange-nvim": {
+ "disp_name": "Melange",
+ "url": "savq/melange-nvim",
+ "colorschemes": ["melange"]
+ },
+ "tokyodark.nvim": {
+ "disp_name": "Tokyodark",
+ "url": "tiagovla/tokyodark.nvim",
+ "colorschemes": ["tokyodark"]
+ },
+ "gruvbox-baby": {
+ "disp_name": "Gruvbox Baby",
+ "url": "luisiacc/gruvbox-baby",
+ "colorschemes": ["gruvbox-baby"]
+ },
+ "monokai.nvim": {
+ "disp_name": "Monokai Neovim",
+ "url": "tanvirtin/monokai.nvim",
+ "colorschemes": [
+ "monokai",
+ { "name": "monokai_pro", "disp_name": "Monokai Pro" },
+ { "name": "monokai_soda", "disp_name": "Monokai Soda" },
+ { "name": "monokai_ristretto", "disp_name": "Monokai Ristretto" }
+ ]
+ },
+ "mellifluous.nvim": {
+ "disp_name": "Mellifluous",
+ "url": "ramojus/mellifluous.nvim",
+ "colorschemes": ["mellifluous"]
+ },
+ "night-owl.nvim": {
+ "disp_name": "Nightowl",
+ "url": "oxfist/night-owl.nvim",
+ "colorschemes": ["night-owl"]
+ },
+ "oceanic-next": {
+ "disp_name": "Oceanic Next",
+ "url": "mhartington/oceanic-next",
+ "colorschemes": ["OceanicNext"]
+ },
+ "poimandres.nvim": {
+ "disp_name": "Poimandres",
+ "url": "olivercederborg/poimandres.nvim",
+ "colorschemes": ["poimandres"]
+ },
+ "cyberdream.nvim": {
+ "disp_name": "Cyberdream",
+ "url": "scottmckendry/cyberdream.nvim",
+ "colorschemes": ["cyberdream"]
+ },
+ "mellow.nvim": {
+ "disp_name": "Mellow",
+ "url": "kvrohit/mellow.nvim",
+ "colorschemes": ["mellow"]
+ },
+ "rasmus.nvim": {
+ "disp_name": "Rasmus",
+ "url": "kvrohit/rasmus.nvim",
+ "colorschemes": ["rasmus"]
+ },
+ "substrata.nvim": {
+ "disp_name": "Substrata",
+ "url": "kvrohit/substrata.nvim",
+ "colorschemes": ["substrata"]
+ },
+ "bamboo.nvim": {
+ "disp_name": "Bamboo",
+ "url": "ribru17/bamboo.nvim",
+ "colorschemes": [
+ {
+ "lua": "require('bamboo').setup({style='vulgaris'}); vim.cmd.colorscheme('bamboo')"
+ },
+ {
+ "disp_name": "Bamboo Multiplex",
+ "lua": "require('bamboo').setup({style='multiplex'}); vim.cmd.colorscheme('bamboo')"
+ },
+ {
+ "disp_name": "Bamboo Light",
+ "lua": "require('bamboo').setup({style='light'}); vim.cmd.colorscheme('bamboo')"
+ }
+ ]
+ },
+ "nvim-solarized-lua": {
+ "disp_name": "Solarized",
+ "url": "ishan9299/nvim-solarized-lua",
+ "colorschemes": [
+ "solarized",
+ { "name": "solarized-high", "disp_name": "Solarized High" },
+ { "name": "solarized-flat", "disp_name": "Solarized Flat" },
+ { "name": "solarized-low", "disp_name": "Solarized Low" }
+ ]
+ },
+ "neon": {
+ "disp_name": "Neon",
+ "url": "rafamadriz/neon",
+ "colorschemes": [
+ {
+ "lua": "vim.g.neon_style='default'; vim.cmd.colorscheme('neon')"
+ },
+ {
+ "disp_name": "Neon Doom",
+ "lua": "vim.g.neon_style='doom'; vim.cmd.colorscheme('neon')"
+ },
+ {
+ "disp_name": "Neon Dark",
+ "lua": "vim.g.neon_style='dark'; vim.cmd.colorscheme('neon')"
+ },
+ {
+ "disp_name": "Neon Light",
+ "lua": "vim.g.neon_style='light'; vim.cmd.colorscheme('neon')"
+ }
+ ]
+ },
+ "modus-theme-vim": {
+ "disp_name": "Modus Theme (vim)",
+ "url": "ishan9299/modus-theme-vim",
+ "colorschemes": [
+ { "name": "modus-vivendi", "disp_name": "Modus Vivendi (vim)" },
+ { "name": "modus-operandi", "disp_name": "Modus Operandi (vim)" }
+ ]
+ },
+ "modus-themes.nvim": {
+ "disp_name": "Modus Theme (nvim)",
+ "url": "miikanissi/modus-themes.nvim",
+ "colorschemes": [
+ { "name": "modus_vivendi", "disp_name": "Modus Vivendi (nvim)" },
+ { "name": "modus_operandi", "disp_name": "Modus Operandi (nvim)" }
+ ]
+ },
+ "moonlight.nvim": {
+ "disp_name": "Moonlight",
+ "url": "shaunsingh/moonlight.nvim",
+ "colorschemes": ["moonlight"]
+ },
+ "aurora": {
+ "disp_name": "Aurora",
+ "url": "ray-x/aurora",
+ "colorschemes": ["aurora"]
+ },
+ "starry.nvim": {
+ "disp_name": "Starry",
+ "url": "ray-x/starry.nvim",
+ "colorschemes": [
+ {
+ "disp_name": "Starry Material",
+ "lua": "require('starry').setup({style={name='material'}}); vim.cmd.colorscheme('starry')"
+ },
+ {
+ "disp_name": "Starry Moonlight",
+ "lua": "require('starry').setup({style={name='moonlight'}}); vim.cmd.colorscheme('starry')"
+ },
+ {
+ "disp_name": "Starry Darcula",
+ "lua": "require('starry').setup({style={name='dracula'}}); vim.cmd.colorscheme('starry')"
+ },
+ {
+ "disp_name": "Starry Darcula Blood",
+ "lua": "require('starry').setup({style={name='dracula_blood'}}); vim.cmd.colorscheme('starry')"
+ },
+ {
+ "disp_name": "Starry Mariana",
+ "lua": "require('starry').setup({style={name='mariana'}}); vim.cmd.colorscheme('starry')"
+ },
+ {
+ "disp_name": "Starry Emerald",
+ "lua": "require('starry').setup({style={name='emerald'}}); vim.cmd.colorscheme('starry')"
+ },
+ {
+ "disp_name": "Starry Middlenight Blue",
+ "lua": "require('starry').setup({style={name='middlenight_blue'}}); vim.cmd.colorscheme('starry')"
+ },
+ {
+ "disp_name": "Starry Early Summer",
+ "lua": "require('starry').setup({style={name='earlysummer'}}); vim.cmd.colorscheme('starry')"
+ },
+ {
+ "disp_name": "Starry Dark Solar",
+ "lua": "require('starry').setup({style={name='darksolar'}}); vim.cmd.colorscheme('starry')"
+ },
+ {
+ "disp_name": "Starry Ukraine",
+ "lua": "require('starry').setup({style={name='ukraine'}}); vim.cmd.colorscheme('starry')"
+ }
+ ]
+ },
+ "bluloco.nvim": {
+ "disp_name": "Bluloco",
+ "url": "uloco/bluloco.nvim",
+ "colorschemes": [
+ {
+ "disp_name": "Bluloco Dark",
+ "lua": "require('bluloco').setup({style='dark'}); vim.cmd.colorscheme('bluloco')"
+ },
+ {
+ "disp_name": "Bluloco Light",
+ "lua": "require('bluloco').setup({style='light'}); vim.cmd.colorscheme('bluloco')"
+ }
+ ]
+ },
+ "doom-one.nvim": {
+ "disp_name": "Doom One",
+ "url": "NTBBloodbath/doom-one.nvim",
+ "colorschemes": ["doom-one"]
+ },
+ "nvimgelion": {
+ "disp_name": "Nvimgelion",
+ "url": "nyngwang/nvimgelion",
+ "colorschemes": ["nvimgelion"]
+ },
+ "boo-colorscheme-nvim": {
+ "disp_name": "Boo",
+ "url": "rockerBOO/boo-colorscheme-nvim",
+ "colorschemes": ["boo"]
+ },
+ "zephyr-nvim": {
+ "disp_name": "Zephyr",
+ "url": "glepnir/zephyr-nvim",
+ "colorschemes": ["zephyr"]
+ },
+ "everblush.nvim": {
+ "disp_name": "Everblush",
+ "url": "Everblush/everblush.nvim",
+ "colorschemes": ["everblush"]
+ },
+ "blue-moon": {
+ "disp_name": "Blue Moon",
+ "url": "kyazdani42/blue-moon",
+ "colorschemes": ["blue-moon"]
+ },
+ "one_monokai.nvim": {
+ "disp_name": "One Monokai",
+ "url": "cpea2506/one_monokai.nvim",
+ "colorschemes": ["one_monokai"]
+ },
+ "arctic.nvim": {
+ "disp_name": "Arctic",
+ "url": "rockyzhang24/arctic.nvim",
+ "colorschemes": ["arctic"]
+ },
+ "calvera-dark.nvim": {
+ "disp_name": "Calvera Dark",
+ "url": "yashguptaz/calvera-dark.nvim",
+ "colorschemes": ["calvera"]
+ },
+ "adwaita.nvim": {
+ "disp_name": "Adwaita",
+ "url": "Mofiqul/adwaita.nvim",
+ "colorschemes": ["adwaita"]
+ },
+ "synthweave.nvim": {
+ "disp_name": "Synthweave",
+ "url": "samharju/synthweave.nvim",
+ "colorschemes": ["synthweave"]
+ },
+ "minimal.nvim": {
+ "disp_name": "Minimal",
+ "url": "Yazeed1s/minimal.nvim",
+ "colorschemes": [
+ "minimal",
+ { "name": "minimal-base16", "disp_name": "Minimal Base16" }
+ ]
+ },
+ "oh-lucy.nvim": {
+ "disp_name": "Oh Lucy",
+ "url": "Yazeed1s/oh-lucy.nvim",
+ "colorschemes": [
+ "oh-lucy",
+ { "name": "oh-lucy-evening", "disp_name": "Oh Lucy Evening" }
+ ]
+ },
+ "monochrome.nvim": {
+ "disp_name": "Monochrome",
+ "url": "kdheepak/monochrome.nvim",
+ "colorschemes": ["monochrome"]
+ },
+ "miasma.nvim": {
+ "disp_name": "Miasma",
+ "url": "xero/miasma.nvim",
+ "colorschemes": ["miasma"]
+ },
+ "citruszest.nvim": {
+ "disp_name": "Citruszest",
+ "url": "zootedb0t/citruszest.nvim",
+ "colorschemes": ["citruszest"]
+ },
+ "neosolarized.nvim": {
+ "disp_name": "NeoSolarized",
+ "url": "Tsuzat/NeoSolarized.nvim",
+ "colorschemes": [
+ {
+ "disp_name": "NeoSolarized Dark",
+ "lua": "require('NeoSolarized').setup({style='dark'}); vim.cmd.colorscheme('NeoSolarized')"
+ },
+ {
+ "disp_name": "NeoSolarized Light",
+ "lua": "require('NeoSolarized').setup({style='light'}); vim.cmd.colorscheme('NeoSolarized')"
+ }
+ ]
+ },
+ "visual_studio_code": {
+ "disp_name": "Visual Studio Code",
+ "url": "askfiy/visual_studio_code",
+ "colorschemes": [
+ {
+ "disp_name": "Visual Studio Code Dark",
+ "lua": "require('visual_studio_code').setup({mode='dark'}); vim.cmd.colorscheme('visual_studio_code')"
+ },
+ {
+ "disp_name": "Visual Studio Code Light",
+ "lua": "require('visual_studio_code').setup({mode='light'}); vim.cmd.colorscheme('visual_studio_code')"
+ }
+ ]
+ },
+ "falcon": {
+ "disp_name": "Falcon",
+ "url": "fenetikm/falcon",
+ "colorschemes": ["falcon"]
+ },
+ "one-nvim": {
+ "disp_name": "One Neovim",
+ "url": "Th3Whit3Wolf/one-nvim",
+ "colorschemes": ["one-nvim"]
+ },
+ "apprentice.nvim": {
+ "disp_name": "Apprentice",
+ "url": "adisen99/apprentice.nvim",
+ "colorschemes": ["apprentice"]
+ },
+ "zenbones.nvim": {
+ "disp_name": "Zenbones",
+ "url": "mcchrish/zenbones.nvim",
+ "colorschemes": [
+ "zenwritten",
+ { "name": "neobones", "disp_name": "Zenbones Neobones" },
+ { "name": "vimbones", "disp_name": "Zenbones Vimbones" },
+ { "name": "rosebones", "disp_name": "Zenbones Rosebones" },
+ { "name": "forestbones", "disp_name": "Zenbones Forestbones" },
+ { "name": "nordbones", "disp_name": "Zenbones Nordbones" },
+ { "name": "tokyobones", "disp_name": "Zenbones Tokyobones" },
+ { "name": "seolbones", "disp_name": "Zenbones Seolbones" },
+ { "name": "duckbones", "disp_name": "Zenbones Duckbones" },
+ { "name": "zenburned", "disp_name": "Zenbones Zenburned" },
+ { "name": "kanagawabones", "disp_name": "Zenbones Kanagawabones" },
+ { "name": "randombones", "disp_name": "Zenbones Randombones" }
+ ]
+ }
+}
diff --git a/lua/fzf-lua/defaults.lua b/lua/fzf-lua/defaults.lua
index 3d2820b7..94ae846f 100644
--- a/lua/fzf-lua/defaults.lua
+++ b/lua/fzf-lua/defaults.lua
@@ -1,11 +1,12 @@
local path = require "fzf-lua.path"
local utils = require "fzf-lua.utils"
local actions = require "fzf-lua.actions"
+local devicons = require "fzf-lua.devicons"
local previewers = require "fzf-lua.previewer"
local M = {}
-M._has_devicons = utils.__HAS_DEVICONS
+M._has_devicons = devicons.plugin_loaded()
function M._default_previewer_fn()
local previewer = M.globals.default_previewer or M.globals.winopts.preview.default
@@ -17,19 +18,19 @@ function M._default_previewer_fn()
end
function M._preview_pager_fn()
- return vim.fn.executable("delta") == 1 and "delta --width=$FZF_PREVIEW_COLUMNS" or nil
+ return vim.fn.executable("delta") == 1 and "delta --width=$COLUMNS" or nil
end
-M.defaults = {
+M.defaults = {
nbsp = utils.nbsp,
winopts = {
- height = 0.85,
- width = 0.80,
- row = 0.35,
- col = 0.55,
- border = "rounded",
- fullscreen = false,
- preview = {
+ height = 0.85,
+ width = 0.80,
+ row = 0.35,
+ col = 0.55,
+ border = "rounded",
+ fullscreen = false,
+ preview = {
default = "builtin",
border = "border",
wrap = "nowrap",
@@ -61,27 +62,7 @@ M.defaults = {
scrolloff = 1,
},
},
- _borderchars = {
- ["none"] = { " ", " ", " ", " ", " ", " ", " ", " " },
- ["solid"] = { " ", " ", " ", " ", " ", " ", " ", " " },
- ["single"] = { "ā", "ā", "ā", "ā", "ā", "ā", "ā", "ā" },
- ["double"] = { "ā", "ā", "ā", "ā", "ā", "ā", "ā", "ā" },
- ["rounded"] = { "ā", "ā", "ā®", "ā", "āÆ", "ā", "ā°", "ā" },
- ["thicc"] = { "ā", "ā", "ā", "ā", "ā", "ā", "ā", "ā" },
- ["thiccc"] = { "ā", "ā", "ā", "ā", "ā", "ā", "ā", "ā" },
- ["thicccc"] = { "ā", "ā", "ā", "ā", "ā", "ā", "ā", "ā" },
- },
- -- border chars reverse lookup for ambiwidth="double"
- _border2string = {
- [" "] = "solid",
- ["ā"] = "single",
- ["ā"] = "double",
- ["ā"] = "rounded",
- ["ā"] = "double",
- ["ā"] = "double",
- ["ā"] = "double",
- },
- on_create = function()
+ on_create = function()
-- vim.cmd("set winhl=Normal:Normal,FloatBorder:Normal")
end,
},
@@ -131,8 +112,8 @@ M.defaults = {
},
fzf_bin = nil,
fzf_opts = {
- ["--ansi"] = "",
- ["--info"] = "inline",
+ ["--ansi"] = true,
+ ["--info"] = "inline-right",
["--height"] = "100%",
["--layout"] = "reverse",
["--border"] = "none",
@@ -147,18 +128,16 @@ M.defaults = {
bat = {
-- reduce startup time by deferring executable check to previewer constructor (#970)
cmd = function() return vim.fn.executable("batcat") == 1 and "batcat" or "bat" end,
+ -- args = "--color=always --style=default",
args = "--color=always --style=numbers,changes",
_ctor = previewers.fzf.bat_async,
},
bat_native = {
cmd = function() return vim.fn.executable("batcat") == 1 and "batcat" or "bat" end,
args = "--color=always --style=numbers,changes",
- _ctor = previewers.fzf.bat,
- },
- bat_async = {
- cmd = function() return vim.fn.executable("batcat") == 1 and "batcat" or "bat" end,
- args = "--color=always --style=numbers,changes",
- _ctor = previewers.fzf.bat_async,
+ -- NOTE: no support for `bat_native` on Windows, it's a hassle for no real
+ -- benefit, native previewers will be removed from the code at one point
+ _ctor = utils._if_win(previewers.fzf.bat_async, previewers.fzf.bat),
},
head = {
cmd = "head",
@@ -170,6 +149,10 @@ M.defaults = {
cmd_deleted = "git diff --color HEAD --",
cmd_modified = "git diff --color HEAD",
cmd_untracked = "git diff --color --no-index /dev/null",
+ -- TODO: modify previewer code to accept table cmd
+ -- cmd_deleted = { "git", "diff", "--color", "HEAD", "--" },
+ -- cmd_modified = { "git", "diff", "--color", "HEAD" },
+ -- cmd_untracked = { "git", "diff", "--color", "--no-index", "/dev/null" },
_fn_git_icons = function() return M.globals.git.icons end,
_ctor = previewers.fzf.git_diff,
},
@@ -209,7 +192,7 @@ M.defaults = {
},
}
-M.defaults.files = {
+M.defaults.files = {
previewer = M._default_previewer_fn,
prompt = "> ",
cmd = nil, -- default: auto detect find|fd
@@ -220,10 +203,12 @@ M.defaults.files = {
cwd_prompt = true,
cwd_prompt_shorten_len = 32,
cwd_prompt_shorten_val = 1,
- fzf_opts = { ["--info"] = "default", },
- git_status_cmd = { "git", "-c", "color.status=false", "status", "-s" },
+ fzf_opts = { ["--multi"] = true },
+ _fzf_nth_devicons = true,
+ git_status_cmd = {
+ "git", "-c", "color.status=false", "--no-optional-locks", "status", "--porcelain=v1" },
find_opts = [[-type f -not -path '*/\.git/*' -printf '%P\n']],
- rg_opts = "--color=never --files --hidden --follow -g '!.git'",
+ rg_opts = [[--color=never --files --hidden --follow -g "!.git"]],
fd_opts = "--color=never --type f --hidden --follow --exclude .git",
toggle_ignore_flag = "--no-ignore",
_actions = function() return M.globals.actions.files end,
@@ -233,30 +218,34 @@ M.defaults.files = {
-- Must construct our opts table in stages
-- so we can reference 'M.globals.files'
-M.defaults.git = {
+M.defaults.git = {
files = {
- previewer = M._default_previewer_fn,
- prompt = "GitFiles> ",
- cmd = "git ls-files --exclude-standard",
- multiprocess = true,
- file_icons = true and M._has_devicons,
- color_icons = true,
- git_icons = true,
- _actions = function() return M.globals.actions.files end,
- winopts = { preview = { winopts = { cursorline = false } } },
+ previewer = M._default_previewer_fn,
+ prompt = "GitFiles> ",
+ cmd = "git ls-files --exclude-standard",
+ multiprocess = true,
+ file_icons = true and M._has_devicons,
+ color_icons = true,
+ git_icons = true,
+ fzf_opts = { ["--multi"] = true },
+ _fzf_nth_devicons = true,
+ _actions = function() return M.globals.actions.files end,
+ winopts = { preview = { winopts = { cursorline = false } } },
},
status = {
- prompt = "GitStatus> ",
+ prompt = "GitStatus> ",
-- override `color.status=always`, techincally not required
-- since we now also call `utils.strip_ansi_coloring` (#706)
- cmd = "git -c color.status=false status -su",
- previewer = "git_diff",
- multiprocess = true,
- file_icons = true and M._has_devicons,
- color_icons = true,
- git_icons = true,
- _actions = function() return M.globals.actions.files end,
- actions = {
+ cmd = "git -c color.status=false --no-optional-locks status --porcelain=v1 -u",
+ previewer = "git_diff",
+ multiprocess = true,
+ file_icons = true and M._has_devicons,
+ color_icons = true,
+ git_icons = true,
+ fzf_opts = { ["--multi"] = true },
+ _fzf_nth_devicons = true,
+ _actions = function() return M.globals.actions.files end,
+ actions = {
["right"] = { fn = actions.git_unstage, reload = true },
["left"] = { fn = actions.git_stage, reload = true },
["ctrl-x"] = { fn = actions.git_reset, reload = true },
@@ -266,66 +255,71 @@ M.defaults.git = {
},
},
commits = {
- prompt = "Commits> ",
- cmd = "git log --color --pretty=format:'%C(yellow)%h%Creset "
- .. "%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset'",
- preview = "git show --color {1}",
+ prompt = "Commits> ",
+ cmd = [[git log --color --pretty=format:"%C(yellow)%h%Creset ]]
+ .. [[%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset"]],
+ preview = "git show --color {1}",
preview_pager = M._preview_pager_fn,
- actions = {
+ actions = {
["default"] = actions.git_checkout,
["ctrl-y"] = { fn = actions.git_yank_commit, exec_silent = true },
},
- fzf_opts = { ["--no-multi"] = "" },
+ fzf_opts = { ["--no-multi"] = true },
},
bcommits = {
- prompt = "BCommits> ",
- cmd = "git log --color --pretty=format:'%C(yellow)%h%Creset "
- .. "%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset' {file}",
- preview = "git show --color {1} -- {file}",
+ prompt = "BCommits> ",
+ cmd = [[git log --color --pretty=format:"%C(yellow)%h%Creset ]]
+ .. [[%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset" {file}]],
+ preview = "git show --color {1} -- {file}",
preview_pager = M._preview_pager_fn,
- actions = {
+ actions = {
["default"] = actions.git_buf_edit,
["ctrl-s"] = actions.git_buf_split,
["ctrl-v"] = actions.git_buf_vsplit,
["ctrl-t"] = actions.git_buf_tabedit,
["ctrl-y"] = { fn = actions.git_yank_commit, exec_silent = true },
},
- fzf_opts = { ["--no-multi"] = "" },
+ fzf_opts = { ["--no-multi"] = true },
},
branches = {
prompt = "Branches> ",
cmd = "git branch --all --color",
preview = "git log --graph --pretty=oneline --abbrev-commit --color {1}",
- fzf_opts = { ["--no-multi"] = "" },
+ fzf_opts = { ["--no-multi"] = true },
actions = {
["default"] = actions.git_switch,
+ ["ctrl-x"] = { fn = actions.git_branch_del, reload = true },
+ ["ctrl-a"] = { fn = actions.git_branch_add, field_index = "{q}", reload = true },
},
+ cmd_add = { "git", "branch" },
+ cmd_del = { "git", "branch", "--delete" },
},
tags = {
prompt = "Tags> ",
- cmd = "git for-each-ref --color --sort='-taggerdate' --format "
- .. "'%(color:yellow)%(refname:short)%(color:reset) "
- .. "%(color:green)(%(taggerdate:relative))%(color:reset)"
- .. " %(subject) %(color:blue)%(taggername)%(color:reset)' refs/tags",
- preview = "git log --graph --color --pretty=format:'%C(yellow)%h%Creset "
- .. "%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset' {1}",
- fzf_opts = { ["--no-multi"] = "" },
+ cmd = [[git for-each-ref --color --sort="-taggerdate" --format ]]
+ .. [["%(color:yellow)%(refname:short)%(color:reset) ]]
+ .. [[%(color:green)(%(taggerdate:relative))%(color:reset)]]
+ .. [[ %(subject) %(color:blue)%(taggername)%(color:reset)" refs/tags]],
+ preview = [[git log --graph --color --pretty=format:"%C(yellow)%h%Creset ]]
+ .. [[%Cgreen(%><(12)%cr%><|(12))%Creset %s %C(blue)<%an>%Creset" {1}]],
+ fzf_opts = { ["--no-multi"] = true },
actions = { ["default"] = actions.git_checkout },
},
stash = {
- prompt = "Stash> ",
- cmd = "git --no-pager stash list",
- preview = "git --no-pager stash show --patch --color {1}",
- actions = {
+ prompt = "Stash> ",
+ cmd = "git --no-pager stash list",
+ preview = "git --no-pager stash show --patch --color {1}",
+ preview_pager = M._preview_pager_fn,
+ actions = {
["default"] = actions.git_stash_apply,
["ctrl-x"] = { fn = actions.git_stash_drop, reload = true },
},
- fzf_opts = {
+ fzf_opts = {
-- TODO: multiselect requires more work as dropping
-- a stash changes the stash index, causing an error
-- when the next stash is attempted
- ["--no-multi"] = "",
- ["--delimiter"] = "'[:]'",
+ ["--no-multi"] = true,
+ ["--delimiter"] = "[:]",
},
},
icons = {
@@ -339,7 +333,7 @@ M.defaults.git = {
},
}
-M.defaults.grep = {
+M.defaults.grep = {
previewer = M._default_previewer_fn,
prompt = "Rg> ",
input_prompt = "Grep For> ",
@@ -348,7 +342,7 @@ M.defaults.grep = {
file_icons = true and M._has_devicons,
color_icons = true,
git_icons = true,
- fzf_opts = { ["--info"] = "default", },
+ fzf_opts = { ["--multi"] = true },
grep_opts = utils.is_darwin()
and "--binary-files=without-match --line-number --recursive --color=always "
.. "--extended-regexp -e"
@@ -363,63 +357,72 @@ M.defaults.grep = {
glob_separator = "%s%-%-", -- query separator pattern (lua): ' --'
}
-M.defaults.args = {
- previewer = M._default_previewer_fn,
- prompt = "Args> ",
- files_only = true,
- file_icons = true and M._has_devicons,
- color_icons = true,
- git_icons = true,
- _actions = function() return M.globals.actions.files end,
- actions = { ["ctrl-x"] = { fn = actions.arg_del, reload = true } },
+M.defaults.args = {
+ previewer = M._default_previewer_fn,
+ prompt = "Args> ",
+ files_only = true,
+ file_icons = true and M._has_devicons,
+ color_icons = true,
+ git_icons = true,
+ fzf_opts = { ["--multi"] = true },
+ _fzf_nth_devicons = true,
+ _actions = function() return M.globals.actions.files end,
+ actions = { ["ctrl-x"] = { fn = actions.arg_del, reload = true } },
}
-M.defaults.oldfiles = {
- previewer = M._default_previewer_fn,
- prompt = "History> ",
- file_icons = true and M._has_devicons,
- color_icons = true,
- git_icons = false,
- stat_file = true,
- fzf_opts = { ["--tiebreak"] = "index", },
- _actions = function() return M.globals.actions.files end,
+M.defaults.oldfiles = {
+ previewer = M._default_previewer_fn,
+ prompt = "History> ",
+ file_icons = true and M._has_devicons,
+ color_icons = true,
+ git_icons = false,
+ stat_file = true,
+ fzf_opts = { ["--tiebreak"] = "index", ["--multi"] = true },
+ _fzf_nth_devicons = true,
+ _actions = function() return M.globals.actions.files end,
}
-M.defaults.quickfix = {
+M.defaults.quickfix = {
previewer = M._default_previewer_fn,
prompt = "Quickfix> ",
separator = "ā",
file_icons = true and M._has_devicons,
color_icons = true,
git_icons = false,
+ fzf_opts = { ["--multi"] = true },
_actions = function() return M.globals.actions.files end,
+ only_valid = false,
}
-M.defaults.quickfix_stack = {
+M.defaults.quickfix_stack = {
prompt = "Quickfix Stack> ",
marker = ">",
previewer = { _ctor = previewers.builtin.quickfix, },
+ fzf_opts = { ["--no-multi"] = true },
actions = { ["default"] = actions.set_qflist, },
}
-M.defaults.loclist = {
+M.defaults.loclist = {
previewer = M._default_previewer_fn,
prompt = "Locations> ",
separator = "ā",
file_icons = true and M._has_devicons,
color_icons = true,
git_icons = false,
+ fzf_opts = { ["--multi"] = true },
_actions = function() return M.globals.actions.files end,
+ only_valid = false,
}
-M.defaults.loclist_stack = {
+M.defaults.loclist_stack = {
prompt = "Locations Stack> ",
marker = ">",
previewer = { _ctor = previewers.builtin.quickfix, },
+ fzf_opts = { ["--no-multi"] = true },
actions = { ["default"] = actions.set_qflist, },
}
-M.defaults.buffers = {
+M.defaults.buffers = {
previewer = M._default_previewer_fn,
prompt = "Buffers> ",
file_icons = true and M._has_devicons,
@@ -430,13 +433,13 @@ M.defaults.buffers = {
no_action_set_cursor = true,
cwd_only = false,
cwd = nil,
- fzf_opts = { ["--tiebreak"] = "index", },
+ fzf_opts = { ["--tiebreak"] = "index", ["--multi"] = true },
_actions = function() return M.globals.actions.buffers end,
actions = { ["ctrl-x"] = { fn = actions.buf_del, reload = true } },
_cached_hls = { "buf_nr", "buf_flag_cur", "buf_flag_alt" },
}
-M.defaults.tabs = {
+M.defaults.tabs = {
previewer = M._default_previewer_fn,
prompt = "Tabs> ",
tab_title = "Tab",
@@ -449,13 +452,14 @@ M.defaults.tabs = {
["ctrl-x"] = { fn = actions.buf_del, reload = true },
},
fzf_opts = {
- ["--delimiter"] = "'[\\):]'",
+ ["--multi"] = true,
+ ["--delimiter"] = "[\\):]",
["--with-nth"] = "3..",
},
_cached_hls = { "buf_nr", "buf_flag_cur", "buf_flag_alt", "tab_title", "tab_marker" },
}
-M.defaults.lines = {
+M.defaults.lines = {
previewer = M._default_previewer_fn,
prompt = "Lines> ",
file_icons = true and M._has_devicons,
@@ -464,7 +468,8 @@ M.defaults.lines = {
show_unlisted = false,
no_term_buffers = true,
fzf_opts = {
- ["--delimiter"] = "'[\\]:]'",
+ ["--no-multi"] = true,
+ ["--delimiter"] = "[\\]:]",
["--nth"] = "2..",
["--tiebreak"] = "index",
["--tabstop"] = "1",
@@ -479,7 +484,7 @@ M.defaults.lines = {
_cached_hls = { "buf_name", "buf_nr", "buf_linenr" },
}
-M.defaults.blines = {
+M.defaults.blines = {
previewer = M._default_previewer_fn,
prompt = "BLines> ",
file_icons = false,
@@ -487,7 +492,8 @@ M.defaults.blines = {
show_unlisted = true,
no_term_buffers = false,
fzf_opts = {
- ["--delimiter"] = "'[:]'",
+ ["--no-multi"] = true,
+ ["--delimiter"] = "[:]",
["--with-nth"] = "2..",
["--tiebreak"] = "index",
["--tabstop"] = "1",
@@ -502,7 +508,7 @@ M.defaults.blines = {
_cached_hls = { "buf_name", "buf_nr", "buf_linenr" },
}
-M.defaults.tags = {
+M.defaults.tags = {
previewer = { _ctor = previewers.builtin.tags },
prompt = "Tags> ",
input_prompt = "[tags] Grep For> ",
@@ -514,52 +520,74 @@ M.defaults.tags = {
git_icons = false,
color_icons = true,
fzf_opts = {
- ["--delimiter"] = string.format("'[:%s]'", utils.nbsp),
+ ["--no-multi"] = true,
+ ["--delimiter"] = string.format("[:%s]", utils.nbsp),
["--tiebreak"] = "begin",
- ["--info"] = "default",
},
_actions = function() return M.globals.actions.files end,
actions = { ["ctrl-g"] = { actions.grep_lgrep } },
}
-M.defaults.btags = {
- previewer = { _ctor = previewers.builtin.tags },
- prompt = "BTags> ",
- ctags_file = nil, -- auto-detect
- rg_opts = "--color=never --no-heading",
- grep_opts = "--color=never --perl-regexp",
- multiprocess = true,
- file_icons = false,
- git_icons = false,
- color_icons = true,
- fzf_opts = {
- ["--delimiter"] = string.format("'[:%s]'", utils.nbsp),
+M.defaults.btags = {
+ previewer = { _ctor = previewers.builtin.tags },
+ prompt = "BTags> ",
+ ctags_file = nil, -- auto-detect
+ rg_opts = "--color=never --no-heading",
+ grep_opts = "--color=never --perl-regexp",
+ multiprocess = true,
+ file_icons = false,
+ git_icons = false,
+ color_icons = true,
+ ctags_autogen = true,
+ fzf_opts = {
+ ["--no-multi"] = true,
+ ["--delimiter"] = string.format("[:%s]", utils.nbsp),
["--with-nth"] = "1,-1",
["--tiebreak"] = "begin",
- ["--info"] = "default",
},
- _actions = function() return M.globals.actions.files end,
- actions = { ["ctrl-g"] = false },
+ _actions = function() return M.globals.actions.files end,
+ actions = { ["ctrl-g"] = false },
}
-M.defaults.colorschemes = {
+M.defaults.colorschemes = {
prompt = "Colorschemes> ",
live_preview = true,
- actions = {
- ["default"] = actions.colorscheme,
- },
- winopts = {
- height = 0.55,
- width = 0.50,
- },
+ winopts = { height = 0.55, width = 0.50 },
+ fzf_opts = { ["--no-multi"] = true },
+ actions = { ["default"] = actions.colorscheme },
}
-M.defaults.highlights = {
+M.defaults.highlights = {
prompt = "Highlights> ",
+ fzf_opts = { ["--no-multi"] = true },
previewer = { _ctor = previewers.builtin.highlights, },
}
-M.defaults.helptags = {
+M.defaults.awesome_colorschemes = {
+ prompt = "Awesome Colorschemes> ",
+ winopts = { row = 0, col = 0.99, width = 0.50 },
+ live_preview = true,
+ max_threads = 5,
+ fzf_opts = {
+ ["--multi"] = true,
+ ["--delimiter"] = "[:]",
+ ["--with-nth"] = "3..",
+ ["--tiebreak"] = "index",
+ },
+ dbfile = "data/colorschemes.json",
+ icons = { utils.ansi_codes.blue("ó°"), utils.ansi_codes.yellow("ī«"), " " },
+ packpath = function()
+ return path.join({ vim.fn.stdpath("cache"), "fzf-lua" })
+ end,
+ actions = {
+ ["default"] = actions.colorscheme,
+ ["ctrl-g"] = { fn = actions.toggle_bg, exec_silent = true },
+ ["ctrl-r"] = { fn = actions.cs_update, reload = true },
+ ["ctrl-x"] = { fn = actions.cs_delete, reload = true },
+ }
+}
+
+M.defaults.helptags = {
prompt = "Help> ",
actions = {
["default"] = actions.help,
@@ -568,15 +596,17 @@ M.defaults.helptags = {
["ctrl-t"] = actions.help_tab,
},
fzf_opts = {
- ["--delimiter"] = "'[ ]'",
+ ["--no-multi"] = true,
+ ["--delimiter"] = string.format("[%s]", utils.nbsp),
["--with-nth"] = "..-2",
+ ["--tiebreak"] = "begin",
},
previewer = {
_ctor = previewers.builtin.help_tags,
},
}
-M.defaults.manpages = {
+M.defaults.manpages = {
prompt = "Man> ",
cmd = "man -k .",
actions = {
@@ -585,11 +615,11 @@ M.defaults.manpages = {
["ctrl-v"] = actions.man_vert,
["ctrl-t"] = actions.man_tab,
},
- fzf_opts = { ["--tiebreak"] = "begin" },
+ fzf_opts = { ["--tiebreak"] = "begin", ["--no-multi"] = true },
previewer = "man",
}
-M.defaults.lsp = {
+M.defaults.lsp = {
previewer = M._default_previewer_fn,
prompt_postfix = "> ",
file_icons = true and M._has_devicons,
@@ -597,10 +627,11 @@ M.defaults.lsp = {
git_icons = false,
cwd_only = false,
async_or_timeout = 5000,
+ fzf_opts = { ["--multi"] = true },
_actions = function() return M.globals.actions.files end,
}
-M.defaults.lsp.symbols = {
+M.defaults.lsp.symbols = {
previewer = M._default_previewer_fn,
prompt_postfix = "> ",
file_icons = true and M._has_devicons,
@@ -642,9 +673,9 @@ M.defaults.lsp.symbols = {
exec_empty_query = true,
-- new formatting options with symbol name at the start
fzf_opts = {
- ["--delimiter"] = string.format("'[:%s]'", utils.nbsp),
+ ["--delimiter"] = string.format("[:%s]", utils.nbsp),
["--tiebreak"] = "begin",
- ["--info"] = "default",
+ ["--no-multi"] = true,
},
line_field_index = "{-2}", -- line field index
field_index_expr = "{}", -- entry field index
@@ -666,12 +697,12 @@ M.defaults.lsp.symbols = {
},
_actions = function() return M.globals.actions.files end,
actions = { ["ctrl-g"] = { actions.sym_lsym } },
+ _cached_hls = { "live_sym" },
}
-M.defaults.lsp.finder = {
+M.defaults.lsp.finder = {
previewer = M._default_previewer_fn,
prompt = "LSP Finder> ",
- fzf_opts = { ["--info"] = "default" },
file_icons = true and M._has_devicons,
color_icons = true,
git_icons = false,
@@ -701,14 +732,15 @@ M.defaults.lsp.finder = {
},
}
-M.defaults.lsp.code_actions = {
+M.defaults.lsp.code_actions = {
prompt = "Code Actions> ",
async_or_timeout = 5000,
previewer = "codeaction",
-- previewer = "codeaction_native",
+ fzf_opts = { ["--no-multi"] = true },
}
-M.defaults.diagnostics = {
+M.defaults.diagnostics = {
previewer = M._default_previewer_fn,
prompt = "Diagnostics> ",
file_icons = true and M._has_devicons,
@@ -717,6 +749,7 @@ M.defaults.diagnostics = {
diag_icons = true,
diag_source = false,
multiline = true,
+ fzf_opts = { ["--multi"] = true },
_actions = function() return M.globals.actions.files end,
-- signs = {
-- ["Error"] = { text = "e", texthl = "DiagnosticError" },
@@ -726,113 +759,106 @@ M.defaults.diagnostics = {
-- },
}
-M.defaults.builtin = {
- prompt = "Builtin> ",
- winopts = {
+M.defaults.builtin = {
+ prompt = "Builtin> ",
+ winopts = {
height = 0.65,
width = 0.50,
},
- actions = {
- ["default"] = actions.run_builtin,
- },
+ fzf_opts = { ["--no-multi"] = true },
+ actions = { ["default"] = actions.run_builtin },
}
-M.defaults.profiles = {
+M.defaults.profiles = {
previewer = M._default_previewer_fn,
prompt = "FzfLua profiles> ",
fzf_opts = {
- ["--delimiter"] = "'[:]'",
- ["--with-nth"] = "2..",
- },
- actions = {
- ["default"] = actions.apply_profile,
+ ["--delimiter"] = "[:]",
+ ["--with-nth"] = "-1..",
+ ["--no-multi"] = true,
},
+ actions = { ["default"] = actions.apply_profile },
}
-M.defaults.marks = {
+M.defaults.marks = {
prompt = "Marks> ",
- actions = {
- ["default"] = actions.goto_mark,
- },
- previewer = {
- _ctor = previewers.builtin.marks,
- },
+ fzf_opts = { ["--no-multi"] = true },
+ actions = { ["default"] = actions.goto_mark },
+ previewer = { _ctor = previewers.builtin.marks },
}
-M.defaults.changes = {
+M.defaults.changes = {
cmd = "changes",
prompt = "Changes> ",
h1 = "change",
}
-M.defaults.jumps = {
+M.defaults.jumps = {
prompt = "Jumps> ",
cmd = "jumps",
- actions = {
- ["default"] = actions.goto_jump,
- },
- previewer = {
- _ctor = previewers.builtin.jumps,
- },
+ fzf_opts = { ["--no-multi"] = true },
+ actions = { ["default"] = actions.goto_jump },
+ previewer = { _ctor = previewers.builtin.jumps },
}
-M.defaults.tagstack = {
+M.defaults.tagstack = {
prompt = "Tagstack> ",
file_icons = true and M._has_devicons,
color_icons = true,
git_icons = true,
+ fzf_opts = { ["--multi"] = true },
previewer = M._default_previewer_fn,
_actions = function() return M.globals.actions.files end,
}
-M.defaults.commands = {
+M.defaults.commands = {
prompt = "Commands> ",
actions = {
["default"] = actions.ex_run,
},
}
-M.defaults.autocmds = {
+M.defaults.autocmds = {
prompt = "Autocmds> ",
previewer = { _ctor = previewers.builtin.autocmds },
_actions = function() return M.globals.actions.files end,
fzf_opts = {
- ["--delimiter"] = "'[:]'",
- ["--with-nth"] = "3..",
+ ["--delimiter"] = "[|]",
+ ["--with-nth"] = "2..",
+ ["--no-multi"] = true,
},
}
-M.defaults.command_history = {
+M.defaults.command_history = {
prompt = "Command History> ",
- fzf_opts = { ["--tiebreak"] = "index", },
+ fzf_opts = { ["--tiebreak"] = "index", ["--no-multi"] = true },
actions = {
["default"] = actions.ex_run_cr,
["ctrl-e"] = actions.ex_run,
},
}
-M.defaults.search_history = {
+M.defaults.search_history = {
prompt = "Search History> ",
- fzf_opts = { ["--tiebreak"] = "index", },
+ fzf_opts = { ["--tiebreak"] = "index", ["--no-multi"] = true },
actions = {
["default"] = actions.search_cr,
["ctrl-e"] = actions.search,
},
}
-M.defaults.registers = {
+M.defaults.registers = {
prompt = "Registers> ",
ignore_empty = true,
- actions = {
- ["default"] = actions.paste_register,
- },
+ actions = { ["default"] = actions.paste_register },
+ fzf_opts = { ["--no-multi"] = true },
}
-M.defaults.keymaps = {
+M.defaults.keymaps = {
prompt = "Keymaps> ",
previewer = { _ctor = previewers.builtin.keymaps },
winopts = { preview = { layout = "vertical" } },
- fzf_opts = { ["--tiebreak"] = "index", },
+ fzf_opts = { ["--tiebreak"] = "index", ["--no-multi"] = true },
ignore_patterns = { "^", "^" },
actions = {
["default"] = actions.keymap_apply,
@@ -842,102 +868,108 @@ M.defaults.keymaps = {
},
}
-M.defaults.spell_suggest = {
+M.defaults.spell_suggest = {
prompt = "Spelling Suggestions> ",
actions = {
["default"] = actions.spell_apply,
},
}
-M.defaults.filetypes = {
+M.defaults.filetypes = {
prompt = "Filetypes> ",
actions = {
["default"] = actions.set_filetype,
},
}
-M.defaults.packadd = {
+M.defaults.packadd = {
prompt = "packadd> ",
actions = {
["default"] = actions.packadd,
},
}
-M.defaults.menus = {
+M.defaults.menus = {
prompt = "Menu> ",
actions = {
["default"] = actions.exec_menu,
},
}
-M.defaults.tmux = {
+M.defaults.tmux = {
buffers = {
prompt = "Tmux Buffers> ",
cmd = "tmux list-buffers",
register = [["]],
- actions = {
- ["default"] = actions.tmux_buf_set_reg,
- },
+ actions = { ["default"] = actions.tmux_buf_set_reg },
+ fzf_opts = { ["--no-multi"] = true, ["--delimiter"] = "[:]" }
},
}
-M.defaults.dap = {
+M.defaults.dap = {
commands = {
prompt = "DAP Commands> ",
+ fzf_opts = { ["--no-multi"] = true },
},
configurations = {
prompt = "DAP Configurations> ",
+ fzf_opts = { ["--no-multi"] = true },
},
variables = {
prompt = "DAP Variables> ",
+ fzf_opts = { ["--no-multi"] = true },
},
frames = {
prompt = "DAP Frames> ",
+ fzf_opts = { ["--no-multi"] = true },
},
breakpoints = {
prompt = "DAP Breakpoints> ",
file_icons = true and M._has_devicons,
color_icons = true,
- git_icons = true,
+ git_icons = false,
previewer = M._default_previewer_fn,
_actions = function() return M.globals.actions.files end,
+ actions = { ["ctrl-x"] = { fn = actions.dap_bp_del, reload = true } },
fzf_opts = {
- ["--delimiter"] = "'[\\]:]'",
+ ["--delimiter"] = "[\\]:]",
["--with-nth"] = "2..",
},
},
}
-M.defaults.complete_path = {
- cmd = nil, -- default: auto detect fd|rg|find
- file_icons = false,
- git_icons = false,
- color_icons = true,
- actions = { ["default"] = actions.complete },
+M.defaults.complete_path = {
+ cmd = nil, -- default: auto detect fd|rg|find
+ file_icons = false,
+ git_icons = false,
+ color_icons = true,
+ multiprocess = true,
+ fzf_opts = { ["--no-multi"] = true },
+ _fzf_nth_devicons = true,
+ actions = { ["default"] = actions.complete },
}
-M.defaults.complete_file = {
- cmd = nil, -- default: auto detect rg|fd|find
- multiprocess = true,
- file_icons = true and M._has_devicons,
- color_icons = true,
- git_icons = false,
- _actions = function() return M.globals.actions.files end,
- actions = { ["default"] = actions.complete },
- previewer = M._default_previewer_fn,
- winopts = { preview = { hidden = "hidden" } },
+M.defaults.complete_file = {
+ cmd = nil, -- default: auto detect rg|fd|find
+ multiprocess = true,
+ file_icons = true and M._has_devicons,
+ color_icons = true,
+ git_icons = false,
+ _actions = function() return M.globals.actions.files end,
+ actions = { ["default"] = actions.complete },
+ previewer = M._default_previewer_fn,
+ winopts = { preview = { hidden = "hidden" } },
+ fzf_opts = { ["--no-multi"] = true },
+ _fzf_nth_devicons = true,
}
-M.defaults.complete_line = { complete = true }
-
-M.defaults.file_icon_padding = ""
+M.defaults.complete_line = { complete = true }
-M.defaults.file_icon_colors = {}
+M.defaults.file_icon_padding = ""
-M.defaults.dir_icon = "īæ"
-M.defaults.dir_icon_color = "#519aba"
+M.defaults.dir_icon = "ļ"
-M.defaults.__HLS = {
+M.defaults.__HLS = {
normal = "FzfLuaNormal",
border = "FzfLuaBorder",
title = "FzfLuaTitle",
@@ -964,6 +996,30 @@ M.defaults.__HLS = {
tab_title = "FzfLuaTabTitle",
tab_marker = "FzfLuaTabMarker",
dir_icon = "FzfLuaDirIcon",
+ live_sym = "FzfLuaLiveSym",
+}
+
+M.defaults.__WINOPTS = {
+ borderchars = {
+ ["none"] = { " ", " ", " ", " ", " ", " ", " ", " " },
+ ["solid"] = { " ", " ", " ", " ", " ", " ", " ", " " },
+ ["single"] = { "ā", "ā", "ā", "ā", "ā", "ā", "ā", "ā" },
+ ["double"] = { "ā", "ā", "ā", "ā", "ā", "ā", "ā", "ā" },
+ ["rounded"] = { "ā", "ā", "ā®", "ā", "āÆ", "ā", "ā°", "ā" },
+ ["thicc"] = { "ā", "ā", "ā", "ā", "ā", "ā", "ā", "ā" },
+ ["thiccc"] = { "ā", "ā", "ā", "ā", "ā", "ā", "ā", "ā" },
+ ["thicccc"] = { "ā", "ā", "ā", "ā", "ā", "ā", "ā", "ā" },
+ },
+ -- border chars reverse lookup for ambiwidth="double"
+ _border2string = {
+ [" "] = "solid",
+ ["ā"] = "single",
+ ["ā"] = "double",
+ ["ā"] = "rounded",
+ ["ā"] = "double",
+ ["ā"] = "double",
+ ["ā"] = "double",
+ },
}
return M
diff --git a/lua/fzf-lua/devicons.lua b/lua/fzf-lua/devicons.lua
new file mode 100644
index 00000000..4d882ddf
--- /dev/null
+++ b/lua/fzf-lua/devicons.lua
@@ -0,0 +1,253 @@
+local path = require "fzf-lua.path"
+local utils = require "fzf-lua.utils"
+
+-- Our "copy" of the devicons library functions so we can load the library
+-- from the headless instance and better support edge cases like multi-part
+-- extension names (#1053)
+-- Not that it makes much difference at this point but this also lowers the
+-- minimum requirements of neovim 0.7 as we no longer need to run setup
+-- and fail due to using the newer highlight creation APIs
+local M = {}
+
+M.plugin_loaded = function()
+ return M.__HAS_DEVICONS
+end
+
+M.plugin_path = function()
+ return M.__DEVICONS_PATH
+end
+
+M.load_devicons_plugin = function()
+ if M.plugin_loaded() then return end
+ -- limit devicons support to nvim >=0.8, although official support is >=0.7
+ -- running setup on 0.7 errs with "W18: Invalid character in group name"
+ if utils.__HAS_NVIM_07 then
+ M.__HAS_DEVICONS, M.__DEVICONS_LIB = pcall(require, "nvim-web-devicons")
+ if M.__HAS_DEVICONS then
+ M.__DEVICONS_PATH = path.parent(path.parent(path.normalize(
+ debug.getinfo(M.__DEVICONS_LIB.setup, "S").source:gsub("^@", ""))))
+ end
+ end
+end
+
+-- Load devicons at least once on require
+M.load_devicons_plugin()
+
+M.load_devicons_fzflua_server = function()
+ local res = nil
+ local ok, errmsg = pcall(function()
+ ---@diagnostic disable-next-line: undefined-field
+ local chan_id = vim.fn.sockconnect("pipe", _G._fzf_lua_server, { rpc = true })
+ res = vim.rpcrequest(
+ chan_id,
+ "nvim_exec_lua",
+ "return require'fzf-lua.devicons'.STATE",
+ {})
+ vim.fn.chanclose(chan_id)
+ end)
+ if not ok or type(res) ~= "table" then
+ io.stderr:write(string.format("RPC error getting fzf_lua:devicons:STATE: %s\n", errmsg))
+ return
+ else
+ M.STATE = res
+ end
+end
+
+M.load_icons = function()
+ if not M.plugin_loaded() then return end
+ if M.STATE and M.STATE.icons
+ -- Refresh if `bg` changed from dark/light (#855)
+ and (not M.STATE.bg or vim.o.bg == M.STATE.bg) then
+ return
+ end
+ -- save the current background
+ M.STATE.bg = vim.o.bg
+ -- rpc request cannot return a table that has mixed elements
+ -- of both indexed items and key value, it will fail with
+ -- "Cannot convert given lua table"
+ -- NOTES:
+ -- (1) devicons.get_icons() returns the default icon in [1]
+ -- (2) we cannot rely on having either .name or .color (#817)
+ local ok, all_devicons = pcall(function()
+ M.__DEVICONS_LIB.refresh() -- reloads light|dark theme
+ return M.__DEVICONS_LIB.get_icons()
+ end)
+ if not ok or not all_devicons or vim.tbl_isempty(all_devicons) then
+ -- something is wrong with devicons
+ -- can't use `error` due to fast event
+ print("[Fzf-lua] error: devicons.get_icons() is nil or empty!")
+ return
+ end
+ local theme
+ if vim.o.background == "light" then
+ ok, theme = pcall(require, "nvim-web-devicons.icons-light")
+ else
+ ok, theme = pcall(require, "nvim-web-devicons.icons-default")
+ end
+ if not ok or type(theme) ~= "table" or not theme.icons_by_filename then
+ print("[Fzf-lua] error: devicons.theme is nil or empty!")
+ return
+ end
+ local icons_by_filename = theme.icons_by_filename or {}
+ local icons_by_file_extension = theme.icons_by_file_extension or {}
+ if type(all_devicons[1]) == "table" then
+ M.STATE.default_icon.icon = all_devicons[1].icon or M.STATE.default_icon.icon
+ M.STATE.default_icon.color = all_devicons[1].color or M.STATE.default_icon.color
+ end
+ M.STATE.icons = {
+ by_filename = {}, -- full filename (path.tail) lookup
+ by_ext = {}, -- simple extension lookup
+ by_ext_2part = {}, -- 2-part extensions, e.g. "foo.test.js"
+ -- lookup table to indicate extension has potentially has better match
+ -- in the 2part for example, ".js" will send us looking for "test.js"
+ ext_has_2part = {},
+
+ }
+ for k, v in pairs(all_devicons) do
+ -- skip all indexed (numeric) entries
+ if type(k) == "string" then
+ local info = {
+ -- NOTE: we no longer need name since we use the RGB color directly
+ -- name = v.name or k,
+ icon = v.icon or "ļ¶",
+ color = v.color or (function()
+ -- some devicons customizations remove `info.color`
+ -- retrieve the color from the highlight group (#801)
+ local hlgroup = "DevIcon" .. (v.name or k)
+ local hexcol = utils.hexcol_from_hl(hlgroup, "fg")
+ if hexcol and #hexcol > 0 then
+ return hexcol
+ end
+ end)(),
+ }
+ -- NOTE: entries like "R" can appear in both icons by filename/extension
+ if icons_by_filename[k] then
+ M.STATE.icons.by_filename[k] = info
+ end
+ if icons_by_file_extension[k] then
+ if k:match(".+%.") then
+ M.STATE.icons.by_ext_2part[k] = info
+ M.STATE.icons.ext_has_2part[path.extension(k)] = true
+ else
+ M.STATE.icons.by_ext[k] = info
+ end
+ end
+ -- if not icons_by_file_extension[k] and not icons_by_filename[k] then
+ -- print("icons_by_operating_system", k)
+ -- end
+ end
+ end
+ return M.STATE.icons
+end
+
+---@param filepath string
+---@param extensionOverride string?
+---@return string, string?
+M.get_devicon = function(filepath, extensionOverride)
+ if not M.STATE or not M.STATE.icons then
+ return unpack({ "", nil })
+ end
+
+ if path.ends_with_separator(filepath) then
+ -- path is directory
+ return M.STATE.dir_icon.icon, M.STATE.dir_icon.color
+ end
+
+ local icon, color
+ local filename = path.tail(filepath)
+ local ext = extensionOverride or path.extension(filename, true)
+
+ -- lookup directly by filename
+ local by_filename = M.STATE.icons.by_filename[filename]
+ if by_filename then
+ icon, color = by_filename.icon, by_filename.color
+ end
+
+ -- check for `ext` as extension can be nil, e.g. "dockerfile"
+ -- lookup by 2 part extensions, e.g. "foo.test.tsx"
+ if ext and not icon and M.STATE.icons.ext_has_2part[ext] then
+ local ext2 = path.extension(filename:sub(1, #filename - #ext - 1))
+ if ext2 then
+ local by_ext_2part = M.STATE.icons.by_ext_2part[ext2 .. "." .. ext]
+ if by_ext_2part then
+ icon, color = by_ext_2part.icon, by_ext_2part.color
+ end
+ end
+ end
+
+ -- finally lookup by "one-part" extension (i.e. no dots in ext)
+ if ext and not icon then
+ local by_ext = M.STATE.icons.by_ext[ext]
+ if by_ext then
+ icon, color = by_ext.icon, by_ext.color
+ end
+ end
+
+ -- Default icon/color, we never return nil
+ icon = icon or M.STATE.default_icon.icon
+ color = color or M.STATE.default_icon.color
+
+ if M.STATE.icon_padding then
+ icon = icon .. M.STATE.icon_padding
+ end
+
+ return icon, color
+end
+
+M.load = function(opts)
+ opts = opts or {}
+
+ M.STATE = vim.tbl_deep_extend("force", M.STATE or {}, {
+ icon_padding = type(opts.icon_padding) == "string" and opts.icon_padding or nil,
+ dir_icon = vim.tbl_extend("force", { icon = "ļ", color = nil }, opts.dir_icon or {}),
+ default_icon =
+ vim.tbl_extend("force", { icon = "ļ¶", color = "#6d8086" }, opts.default_icon or {}),
+ })
+
+ -- Check if we're running from the headless instance, attempt to load our
+ -- icons with the RPC response of `get_icons` from the main fzf-lua instance
+ ---@diagnostic disable-next-line: undefined-field
+ if vim.g.fzf_lua_is_headless and not _G._fzf_lua_server and not _G._devicons_path then
+ local errmsg = "fzf-lua fatal: '_G._fzf_lua_server', '_G._devicons_path' both nil\n"
+ io.stderr:write(errmsg)
+ print(errmsg)
+ return
+ end
+ if vim.g.fzf_lua_is_headless and _G._fzf_lua_server then
+ -- headless instance, fzf-lua server exists, attempt
+ -- to load icons from main neovim instance
+ M.load_devicons_fzflua_server()
+ return
+ end
+ if vim.g.fzf_lua_is_headless and _G._devicons_path then
+ -- headless instance, no fzf-lua server was specified
+ -- but we got devicon's lib path, add to runtime path
+ -- so `load_devicons_plugin` can find the library
+ vim.opt.runtimepath:append(_G._devicons_path)
+ end
+ -- Attempt to load devicons plugin
+ M.load_devicons_plugin()
+
+ -- Load custom overrides before loading icons
+ if vim.g.fzf_lua_is_headless
+ ---@diagnostic disable-next-line: undefined-field
+ and _G._devicons_setup and vim.loop.fs_stat(_G._devicons_setup) then
+ ---@diagnostic disable-next-line: undefined-field
+ local file = loadfile(_G._devicons_setup)
+ if file then pcall(file) end
+ end
+
+ -- Load the devicons iconset
+ M.load_icons()
+end
+
+-- For testing
+M.unload = function()
+ M.STATE = nil
+ M.__HAS_DEVICONS = nil
+ M.__DEVICONS_LIB = nil
+ M.__DEVICONS_PATH = nil
+ M.load_devicons_plugin()
+end
+
+return M
diff --git a/lua/fzf-lua/fzf.lua b/lua/fzf-lua/fzf.lua
index f6f71fa6..6a4e8fb4 100644
--- a/lua/fzf-lua/fzf.lua
+++ b/lua/fzf-lua/fzf.lua
@@ -5,6 +5,9 @@
-- https://github.com/vijaymarupudi/nvim-fzf/blob/master/lua/fzf.lua
local uv = vim.loop
+local utils = require "fzf-lua.utils"
+local libuv = require "fzf-lua.libuv"
+
local M = {}
-- workaround to a potential 'tempname' bug? (#222)
@@ -30,6 +33,11 @@ end
-- contents can be either a table with tostring()able items, or a function that
-- can be called repeatedly for values. The latter can use coroutines for async
-- behavior.
+---@param contents string[]|table|function?
+---@param fzf_cli_args string[]
+---@param opts table
+---@return table selected
+---@return integer exit_code
function M.raw_fzf(contents, fzf_cli_args, opts)
if not coroutine.running() then
error("[Fzf-lua] function must be called inside a coroutine.")
@@ -37,8 +45,8 @@ function M.raw_fzf(contents, fzf_cli_args, opts)
if not opts then opts = {} end
local cwd = opts.fzf_cwd or opts.cwd
- local cmd = opts.fzf_bin or "fzf"
- local fifotmpname = tempname()
+ local cmd = { opts.fzf_bin or "fzf" }
+ local fifotmpname = utils.__IS_WINDOWS and utils.windows_pipename() or tempname()
local outputtmpname = tempname()
-- we use a temporary env $FZF_DEFAULT_COMMAND instead of piping
@@ -48,13 +56,17 @@ function M.raw_fzf(contents, fzf_cli_args, opts)
-- instance never terminates which hangs fzf on exit
local FZF_DEFAULT_COMMAND = nil
- if fzf_cli_args then cmd = cmd .. " " .. fzf_cli_args end
- if opts.fzf_cli_args then cmd = cmd .. " " .. opts.fzf_cli_args end
+ utils.tbl_extend(cmd, fzf_cli_args or {})
+ if type(opts.fzf_cli_args) == "table" then
+ utils.tbl_extend(cmd, opts.fzf_cli_args)
+ elseif type(opts.fzf_cli_args) == "string" then
+ utils.tbl_extend(cmd, { opts.fzf_cli_args })
+ end
if contents then
if type(contents) == "string" and #contents > 0 then
if opts.silent_fail ~= false then
- contents = ("%s || true"):format(contents)
+ contents = contents .. " || " .. utils.shell_nop()
end
FZF_DEFAULT_COMMAND = contents
else
@@ -69,24 +81,39 @@ function M.raw_fzf(contents, fzf_cli_args, opts)
local bin_is_sk = opts.fzf_bin and opts.fzf_bin:match("sk$")
local fish_shell = vim.o.shell and vim.o.shell:match("fish$")
if not fish_shell or bin_is_sk then
- cmd = ("%s < %s"):format(cmd, vim.fn.shellescape(fifotmpname))
+ table.insert(cmd, "<")
+ table.insert(cmd, libuv.shellescape(fifotmpname))
else
- FZF_DEFAULT_COMMAND = string.format("cat %s", vim.fn.shellescape(fifotmpname))
+ FZF_DEFAULT_COMMAND = string.format("cat %s", libuv.shellescape(fifotmpname))
end
end
end
- cmd = ("%s > %s"):format(cmd, vim.fn.shellescape(outputtmpname))
+ table.insert(cmd, ">")
+ table.insert(cmd, libuv.shellescape(outputtmpname))
local fd, output_pipe = nil, nil
local finish_called = false
local write_cb_count = 0
+ local windows_pipe_server = nil
+ ---@type function|nil
+ local handle_contents
- -- Create the output pipe
- -- We use tbl for perf reasons, from ':help system':
- -- If {cmd} is a List it runs directly (no 'shell')
- -- If {cmd} is a String it runs in the 'shell'
- vim.fn.system({ "mkfifo", fifotmpname })
+ if utils.__IS_WINDOWS then
+ windows_pipe_server = uv.new_pipe(false)
+ windows_pipe_server:bind(fifotmpname)
+ windows_pipe_server:listen(16, function()
+ output_pipe = uv.new_pipe(false)
+ windows_pipe_server:accept(output_pipe)
+ handle_contents()
+ end)
+ else
+ -- Create the output pipe
+ -- We use tbl for perf reasons, from ':help system':
+ -- If {cmd} is a List it runs directly (no 'shell')
+ -- If {cmd} is a String it runs in the 'shell'
+ vim.fn.system({ "mkfifo", fifotmpname })
+ end
local function finish(_)
-- mark finish once called
@@ -147,6 +174,23 @@ function M.raw_fzf(contents, fzf_cli_args, opts)
end
end
+ handle_contents = vim.schedule_wrap(function()
+ -- this part runs in the background. When the user has selected, it will
+ -- error out, but that doesn't matter so we just break out of the loop.
+ if contents then
+ if type(contents) == "table" then
+ if not vim.tbl_isempty(contents) then
+ write_cb(vim.tbl_map(function(x)
+ return x .. "\n"
+ end, contents))
+ end
+ finish(4)
+ else
+ contents(usr_write_cb(true), usr_write_cb(false), output_pipe)
+ end
+ end
+ end)
+
-- I'm not sure why this happens (probably a neovim bug) but when pressing
-- in quick successsion immediately after opening the window neovim
-- hangs the CPU at 100% at the last `coroutine.yield` before returning from
@@ -197,18 +241,48 @@ function M.raw_fzf(contents, fzf_cli_args, opts)
end
if opts.debug then
- print("[Fzf-lua]: fzf cmd:", cmd)
+ print("[Fzf-lua]: FZF_DEFAULT_COMMAND:", FZF_DEFAULT_COMMAND)
+ print("[Fzf-lua]: fzf cmd:", table.concat(cmd, " "))
end
local co = coroutine.running()
local jobstart = opts.is_fzf_tmux and vim.fn.jobstart or vim.fn.termopen
- jobstart({ "sh", "-c", cmd }, {
+ local shell_cmd = utils.__IS_WINDOWS
+ and { "cmd", "/d", "/e:off", "/f:off", "/v:off", "/c" }
+ or { "sh", "-c" }
+ if utils.__IS_WINDOWS then
+ utils.tbl_extend(shell_cmd, cmd)
+ else
+ table.insert(shell_cmd, table.concat(cmd, " "))
+ end
+ -- This obscure option makes jobstart fail with: "The syntax of the command is incorrect"
+ -- temporarily set to `false`, for more info see `:help shellslash` (#1055)
+ local nvim_opt_shellslash = utils.__WIN_HAS_SHELLSLASH and vim.o.shellslash
+ if nvim_opt_shellslash then vim.o.shellslash = false end
+ jobstart(shell_cmd, {
cwd = cwd,
pty = true,
env = {
- ["SHELL"] = "sh",
+ ["SHELL"] = shell_cmd[1],
["FZF_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND,
["SKIM_DEFAULT_COMMAND"] = FZF_DEFAULT_COMMAND,
+ ["FZF_DEFAULT_OPTS"] = (function()
+ -- Newer style `--preview-window` options in FZF_DEFAULT_OPTS such as:
+ -- --preview-window "right,50%,hidden,<60(up,70%,hidden)"
+ -- prevents our previewer from working properly, since there is never
+ -- a reason to inherit `preview-window` options it can be safely stripped
+ -- from FZF_DEFAULT_OPTS (#1107)
+ local default_opts = os.getenv("FZF_DEFAULT_OPTS")
+ if not default_opts then return end
+ local patterns = { "--preview-window" }
+ for _, p in ipairs(patterns) do
+ -- remove flag end of string
+ default_opts = default_opts:gsub(utils.lua_regex_escape(p) .. "[=%s]+[^%-]+%s-$", "")
+ -- remove flag start/mid of string
+ default_opts = default_opts:gsub(utils.lua_regex_escape(p) .. "[=%s]+.-%s+%-%-", " --")
+ end
+ return default_opts
+ end)()
},
on_exit = function(_, rc, _)
local output = {}
@@ -220,7 +294,13 @@ function M.raw_fzf(contents, fzf_cli_args, opts)
f:close()
end
finish(1)
- vim.fn.delete(fifotmpname)
+ if windows_pipe_server then
+ windows_pipe_server:close()
+ end
+ -- in windows, pipes that are not used are automatically cleaned up
+ if not utils.__IS_WINDOWS then vim.fn.delete(fifotmpname) end
+ -- Windows only, restore `shellslash` if was true before `jobstart`
+ if nvim_opt_shellslash then vim.o.shellslash = nvim_opt_shellslash end
vim.fn.delete(outputtmpname)
if #output == 0 then output = nil end
coroutine.resume(co, output, rc)
@@ -231,26 +311,16 @@ function M.raw_fzf(contents, fzf_cli_args, opts)
if not opts.is_fzf_tmux then
vim.cmd [[set ft=fzf]]
- -- terminal behavior seems to have changed after the introduction
- -- of 'nt' mode (terminal-normal mode) which is included in 0.6
-- https://github.com/neovim/neovim/pull/15878
- -- Preferably I'd like to check if the vim patch is included using
- -- vim.fn.has('patch-8.2.3461')
- -- but this doesn't work for vim patches > 8.1 as explained in:
- -- https://github.com/neovim/neovim/issues/9635
- -- However, since this patch was included in 0.6 we can test
- -- for neovim version 0.6
- -- Beats me why 'nvim_get_mode().mode' still returns 'nt' even
- -- after we're clearly in insert mode or why `:startinsert`
- -- won't change the mode from 'nt' to 't' so we use feedkeys()
- -- instead.
- -- This "retires" 'actions.ensure_insert_mode' and solves the
- -- issue of calling an fzf-lua mapping from insert mode (#429)
-
- if vim.fn.has("nvim-0.6") == 1 then
- vim.cmd([[noautocmd lua vim.api.nvim_feedkeys(]]
- .. [[vim.api.nvim_replace_termcodes("i", true, false, true)]]
- .. [[, 'n', true)]])
+ -- Since patch-8.2.3461 which was released with 0.6 neovim distinguishes between
+ -- Normal mode and Terminal-Normal mode. However, this seems to have also introduced
+ -- a bug with `startinsert`: When fzf-lua reuses interfaces (e.g. called from "builtin"
+ -- or grep<->live_grep toggle) the current mode will be "t" which is Terminal (INSERT)
+ -- mode but our interface is still opened in NORMAL mode, either `startinsert` is not
+ -- working (as it's technically already in INSERT) or creating a new terminal buffer
+ -- within the same window starts in NORMAL mode while returning the wrong `nvim_get_mode`
+ if utils.__HAS_NVIM_06 and vim.api.nvim_get_mode().mode == "t" then
+ utils.feed_keys_termcodes("i")
else
vim.cmd [[startinsert]]
end
@@ -260,26 +330,17 @@ function M.raw_fzf(contents, fzf_cli_args, opts)
goto wait_for_fzf
end
- -- have to open this after there is a reader (termopen)
- -- otherwise this will block
- fd = uv.fs_open(fifotmpname, "w", -1)
- output_pipe = uv.new_pipe(false)
- output_pipe:open(fd)
- -- print(output_pipe:getpeername())
-
- -- this part runs in the background. When the user has selected, it will
- -- error out, but that doesn't matter so we just break out of the loop.
- if contents then
- if type(contents) == "table" then
- if not vim.tbl_isempty(contents) then
- write_cb(vim.tbl_map(function(x) return x .. "\n" end, contents))
- end
- finish(4)
- else
- contents(usr_write_cb(true), usr_write_cb(false), output_pipe)
- end
+ if not utils.__IS_WINDOWS then
+ -- have to open this after there is a reader (termopen)
+ -- otherwise this will block
+ fd = uv.fs_open(fifotmpname, "w", -1)
+ output_pipe = uv.new_pipe(false)
+ output_pipe:open(fd)
+ -- print(output_pipe:getpeername())
+ handle_contents()
end
+
::wait_for_fzf::
return coroutine.yield()
end
diff --git a/lua/fzf-lua/init.lua b/lua/fzf-lua/init.lua
index 7f9765c2..9a4d58aa 100644
--- a/lua/fzf-lua/init.lua
+++ b/lua/fzf-lua/init.lua
@@ -1,24 +1,35 @@
+-- make value truthy so we can load the path module and subsequently
+-- the libuv module without overriding the global require used only
+-- for spawn_stdio headless instances, this way we can call
+-- require("fzf-lua") from test specs (which also run headless)
+vim.g.fzf_lua_directory = ""
+
local path = require "fzf-lua.path"
local utils = require "fzf-lua.utils"
local config = require "fzf-lua.config"
do
- -- using the latest nightly 'NVIM v0.6.0-dev+569-g2ecf0a4c6'
- -- plugin '.vim' initialization sometimes doesn't get called
+ local function source_vimL(path_parts)
+ local vimL_file = path.join(path_parts)
+ if vim.loop.fs_stat(vimL_file) then
+ vim.cmd("source " .. vimL_file)
+ -- print(string.format("loaded '%s'", vimL_file))
+ end
+ end
+
local currFile = debug.getinfo(1, "S").source:gsub("^@", "")
- vim.g.fzf_lua_directory = path.parent(currFile)
+ vim.g.fzf_lua_directory = path.normalize(path.parent(currFile))
+ local fzf_lua_root = path.parent(path.parent(vim.g.fzf_lua_directory))
-- Manually source the vimL script containing ':FzfLua' cmd
- if not vim.g.loaded_fzf_lua then
- local fzf_lua_vim = path.join({
- path.parent(path.parent(vim.g.fzf_lua_directory)),
- "plugin", "fzf-lua.vim"
- })
- if vim.loop.fs_stat(fzf_lua_vim) then
- vim.cmd(("source %s"):format(fzf_lua_vim))
- -- utils.info(("manually loaded '%s'"):format(fzf_lua_vim))
- end
- end
+ -- does nothing if already loaded due to `vim.g.loaded_fzf_lua`
+ source_vimL({ fzf_lua_root, "plugin", "fzf-lua.vim" })
+ -- Autoload scipts dynamically loaded on `vim.fn[fzf_lua#...]` call
+ -- `vim.fn.exists("*fzf_lua#...")` will return 0 unless we manuall source
+ source_vimL({ fzf_lua_root, "autoload", "fzf_lua.vim" })
+ -- Set var post source as the top of the file `require` will return 0
+ -- due to it potentially being loaded before "autoload/fzf_lua.vim"
+ utils.__HAS_AUTOLOAD_FNS = vim.fn.exists("*fzf_lua#getbufinfo") == 1
-- Create a new RPC server (tmp socket) to listen to messages (actions/headless)
-- this is safer than using $NVIM_LISTEN_ADDRESS. If the user is using a custom
@@ -33,6 +44,9 @@ local M = {}
-- Setup fzf-lua's highlights, use `override=true` to reset all highlights
function M.setup_highlights(override)
+ local is_light = vim.o.bg == "light"
+ local bg_changed = config.__HLS_STATE and config.__HLS_STATE.bg ~= vim.o.bg
+ config.__HLS_STATE = { colorscheme = vim.g.colors_name, bg = vim.o.bg }
-- we use `default = true` so calling this function doesn't override the colorscheme
local default = not override
local highlights = {
@@ -52,21 +66,39 @@ function M.setup_highlights(override)
{ "FzfLuaScrollBorderFull", "scrollborder_f", { default = default, link = "FzfLuaBorder" } },
{ "FzfLuaScrollFloatEmpty", "scrollfloat_e", { default = default, link = "PmenuSbar" } },
{ "FzfLuaScrollFloatFull", "scrollfloat_f", { default = default, link = "PmenuThumb" } },
+ { "FzfLuaDirIcon", "dir_icon", { default = default, link = "Directory" } },
-- Fzf terminal hls, colors from `vim.api.nvim_get_color_map()`
- { "FzfLuaHeaderBind", "header_bind", { default = default, fg = "BlanchedAlmond" } },
- { "FzfLuaHeaderText", "header_text", { default = default, fg = "Brown1" } },
+ { "FzfLuaHeaderBind", "header_bind",
+ { default = default, fg = is_light and "MediumSpringGreen" or "BlanchedAlmond" } },
+ { "FzfLuaHeaderText", "header_text",
+ { default = default, fg = is_light and "Brown4" or "Brown1" } },
+ { "FzfLuaLiveSym", "live_sym",
+ { default = default, fg = is_light and "Brown4" or "Brown1" } },
-- Provider specific highlights
- { "FzfLuaBufName", "buf_name", { default = default, fg = "LightMagenta" } },
- { "FzfLuaBufNr", "buf_nr", { default = default, fg = "BlanchedAlmond" } },
- { "FzfLuaBufLineNr", "buf_linenr", { default = default, fg = "MediumSpringGreen" } },
- { "FzfLuaBufFlagCur", "buf_flag_cur", { default = default, fg = "Brown1" } },
- { "FzfLuaBufFlagAlt", "buf_flag_alt", { default = default, fg = "CadetBlue1" } },
- { "FzfLuaTabTitle", "tab_title", { default = default, fg = "LightSkyBlue1", bold = true } },
- { "FzfLuaTabMarker", "tab_marker", { default = default, fg = "BlanchedAlmond", bold = true } },
- { "FzfLuaDirIcon", "dir_icon", { default = default, link = "Directory" } },
+ { "FzfLuaBufName", "buf_name", -- lines|blines (hidden)
+ { default = default, fg = is_light and "DarkOrchid3" or "LightMagenta" } },
+ { "FzfLuaBufLineNr", "buf_linenr", -- lines|blines
+ { default = default, fg = is_light and "MediumSpringGreen" or "MediumSpringGreen" } },
+ { "FzfLuaBufNr", "buf_nr", -- buffers|tabs|lines|blines
+ { default = default, fg = is_light and "AquaMarine3" or "BlanchedAlmond" } },
+ { "FzfLuaBufFlagCur", "buf_flag_cur", -- buffers|tabs
+ { default = default, fg = is_light and "Brown4" or "Brown1" } },
+ { "FzfLuaBufFlagAlt", "buf_flag_alt", -- buffers|tabs
+ { default = default, fg = is_light and "CadetBlue4" or "CadetBlue1" } },
+ { "FzfLuaTabTitle", "tab_title", -- tabs
+ { default = default, fg = is_light and "CadetBlue4" or "LightSkyBlue1", bold = true } },
+ { "FzfLuaTabMarker", "tab_marker", -- tabs
+ { default = default, fg = is_light and "MediumSpringGreen" or "BlanchedAlmond", bold = true } },
}
for _, a in ipairs(highlights) do
local hl_name, _, hl_def = a[1], a[2], a[3]
+ -- If color was a linked colormap and bg changed set definition to override
+ if hl_def.fg and bg_changed then
+ local fg_current = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(hl_name)), "fg")
+ if fg_current and not fg_current:match("^#") and fg_current ~= hl_def.fg then
+ hl_def.default = false
+ end
+ end
if utils.__HAS_NVIM_07 then
vim.api.nvim_set_hl(0, hl_name, hl_def)
else
@@ -106,24 +138,36 @@ end
-- case the user decides not to call `setup()`
M.setup_highlights()
-function M.load_profile(profile)
- local fname = path.join({ vim.g.fzf_lua_directory, "profiles", profile .. ".lua" })
- return utils.load_profile(fname, nil, true)
-end
-
-function M.setup(opts, do_not_reset_defaults)
- opts = type(opts) == "table" and opts or {}
- if type(opts[1]) == "string" then
- -- Did the user request a specific profile?
- local profile_opts = M.load_profile(opts[1])
+local function load_profiles(profiles)
+ local ret = {}
+ profiles = type(profiles) == "table" and profiles
+ or type(profiles) == "string" and { profiles }
+ or {}
+ for _, profile in ipairs(profiles) do
+ local fname = path.join({ vim.g.fzf_lua_directory, "profiles", profile .. ".lua" })
+ local profile_opts = utils.load_profile_fname(fname, nil, true)
if type(profile_opts) == "table" then
+ if profile_opts[1] then
+ -- profile requires loading base profile(s)
+ profile_opts = vim.tbl_deep_extend("keep",
+ profile_opts, load_profiles(profile_opts[1]))
+ end
if type(profile_opts.fn_load) == "function" then
profile_opts.fn_load()
profile_opts.fn_load = nil
end
- opts = vim.tbl_deep_extend("keep", opts, profile_opts)
+ ret = vim.tbl_deep_extend("force", ret, profile_opts)
end
end
+ return ret
+end
+
+function M.setup(opts, do_not_reset_defaults)
+ opts = type(opts) == "table" and opts or {}
+ if opts[1] then
+ -- Did the user supply profile(s) to load?
+ opts = vim.tbl_deep_extend("keep", opts, load_profiles(opts[1]))
+ end
if do_not_reset_defaults then
-- no defaults reset requested, merge with previous setup options
opts = vim.tbl_deep_extend("keep", opts, config.setup_opts or {})
@@ -195,10 +239,14 @@ do
tabs = { "fzf-lua.providers.buffers", "tabs" },
lines = { "fzf-lua.providers.buffers", "lines" },
blines = { "fzf-lua.providers.buffers", "blines" },
+ helptags = { "fzf-lua.providers.helptags", "helptags" },
+ manpages = { "fzf-lua.providers.manpages", "manpages" },
+ -- backward compat
help_tags = { "fzf-lua.providers.helptags", "helptags" },
man_pages = { "fzf-lua.providers.manpages", "manpages" },
colorschemes = { "fzf-lua.providers.colorschemes", "colorschemes" },
highlights = { "fzf-lua.providers.colorschemes", "highlights" },
+ awesome_colorschemes = { "fzf-lua.providers.colorschemes", "awesome_colorschemes" },
jumps = { "fzf-lua.providers.nvim", "jumps" },
changes = { "fzf-lua.providers.nvim", "changes" },
tagstack = { "fzf-lua.providers.nvim", "tagstack" },
@@ -238,13 +286,6 @@ do
deregister_ui_select = { "fzf-lua.providers.ui_select", "deregister" },
tmux_buffers = { "fzf-lua.providers.tmux", "buffers" },
profiles = { "fzf-lua.providers.module", "profiles" },
- -- API shortcuts
- fzf = { "fzf-lua.core", "fzf" },
- fzf_raw = { "fzf-lua.fzf", "raw_fzf" },
- fzf_wrap = { "fzf-lua.core", "fzf_wrap" },
- fzf_exec = { "fzf-lua.core", "fzf_exec" },
- fzf_live = { "fzf-lua.core", "fzf_live" },
- fzf_complete = { "fzf-lua.complete", "fzf_complete" },
complete_path = { "fzf-lua.complete", "path" },
complete_file = { "fzf-lua.complete", "file" },
complete_line = { "fzf-lua.complete", "line" },
@@ -292,6 +333,12 @@ end
-- export the defaults module and deref
M.defaults = require("fzf-lua.defaults").defaults
+-- API shortcuts
+M.fzf_exec = require("fzf-lua.core").fzf_exec
+M.fzf_live = require("fzf-lua.core").fzf_live
+M.fzf_wrap = require("fzf-lua.core").fzf_wrap
+-- M.fzf_raw = require( "fzf-lua.fzf").raw_fzf
+
-- exported modules
M._exported_modules = {
"win",
@@ -307,14 +354,12 @@ M._exported_modules = {
-- excluded from builtin / auto-complete
M._excluded_meta = {
- "load_profile",
"setup",
"fzf",
"fzf_raw",
"fzf_wrap",
"fzf_exec",
"fzf_live",
- "fzf_complete",
"defaults",
"_excluded_meta",
"_excluded_metamap",
@@ -323,6 +368,11 @@ M._excluded_meta = {
"get_info",
"set_info",
"get_last_query",
+ -- Exclude due to rename:
+ -- help_tags -> helptags
+ -- man_pages -> manpages
+ "help_tags",
+ "man_pages",
}
for _, m in ipairs(M._exported_modules) do
diff --git a/lua/fzf-lua/lib/base64.lua b/lua/fzf-lua/lib/base64.lua
new file mode 100644
index 00000000..ce82b6bf
--- /dev/null
+++ b/lua/fzf-lua/lib/base64.lua
@@ -0,0 +1,206 @@
+--[[
+
+Source: https://github.com/iskolbin/lbase64
+
+ base64 -- v1.5.3 public domain Lua base64 encoder/decoder
+ no warranty implied; use at your own risk
+
+ Needs bit32.extract function. If not present it's implemented using BitOp
+ or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua
+ implementation inspired by Rici Lake's post:
+ http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html
+
+ author: Ilya Kolbin (iskolbin@gmail.com)
+ url: github.com/iskolbin/lbase64
+
+ COMPATIBILITY
+
+ Lua 5.1+, LuaJIT
+
+ LICENSE
+
+ See end of file for license information.
+
+--]]
+---@diagnostic disable
+
+local base64 = {}
+
+local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode
+if not extract then
+ if _G.bit then -- LuaJIT
+ local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band
+ extract = function(v, from, width)
+ return band(shr(v, from), shl(1, width) - 1)
+ end
+ elseif _G._VERSION == "Lua 5.1" then
+ extract = function(v, from, width)
+ local w = 0
+ local flag = 2 ^ from
+ for i = 0, width - 1 do
+ local flag2 = flag + flag
+ if v % flag2 >= flag then
+ w = w + 2 ^ i
+ end
+ flag = flag2
+ end
+ return w
+ end
+ else -- Lua 5.3+
+ extract = load [[return function( v, from, width )
+ return ( v >> from ) & ((1 << width) - 1)
+ end]] ()
+ end
+end
+
+
+function base64.makeencoder(s62, s63, spad)
+ local encoder = {}
+ for b64code, char in pairs { [0] = 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
+ 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
+ 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
+ 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2',
+ '3', '4', '5', '6', '7', '8', '9', s62 or '+', s63 or '/', spad or '=' } do
+ encoder[b64code] = char:byte()
+ end
+ return encoder
+end
+
+function base64.makedecoder(s62, s63, spad)
+ local decoder = {}
+ for b64code, charcode in pairs(base64.makeencoder(s62, s63, spad)) do
+ decoder[charcode] = b64code
+ end
+ return decoder
+end
+
+local DEFAULT_ENCODER = base64.makeencoder()
+local DEFAULT_DECODER = base64.makedecoder()
+
+local char, concat = string.char, table.concat
+
+function base64.encode(str, encoder, usecaching)
+ encoder = encoder or DEFAULT_ENCODER
+ local t, k, n = {}, 1, #str
+ local lastn = n % 3
+ local cache = {}
+ for i = 1, n - lastn, 3 do
+ local a, b, c = str:byte(i, i + 2)
+ local v = a * 0x10000 + b * 0x100 + c
+ local s
+ if usecaching then
+ s = cache[v]
+ if not s then
+ s = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[extract(v, 6, 6)],
+ encoder[extract(v, 0, 6)])
+ cache[v] = s
+ end
+ else
+ s = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[extract(v, 6, 6)],
+ encoder[extract(v, 0, 6)])
+ end
+ t[k] = s
+ k = k + 1
+ end
+ if lastn == 2 then
+ local a, b = str:byte(n - 1, n)
+ local v = a * 0x10000 + b * 0x100
+ t[k] = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[extract(v, 6, 6)],
+ encoder[64])
+ elseif lastn == 1 then
+ local v = str:byte(n) * 0x10000
+ t[k] = char(encoder[extract(v, 18, 6)], encoder[extract(v, 12, 6)], encoder[64], encoder[64])
+ end
+ return concat(t)
+end
+
+function base64.decode(b64, decoder, usecaching)
+ decoder = decoder or DEFAULT_DECODER
+ local pattern = '[^%w%+%/%=]'
+ if decoder then
+ local s62, s63
+ for charcode, b64code in pairs(decoder) do
+ if b64code == 62 then s62 = charcode
+ elseif b64code == 63 then s63 = charcode
+ end
+ end
+ pattern = ('[^%%w%%%s%%%s%%=]'):format(char(s62), char(s63))
+ end
+ b64 = b64:gsub(pattern, '')
+ local cache = usecaching and {}
+ local t, k = {}, 1
+ local n = #b64
+ local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0
+ for i = 1, padding > 0 and n - 4 or n, 4 do
+ local a, b, c, d = b64:byte(i, i + 3)
+ local s
+ if usecaching then
+ local v0 = a * 0x1000000 + b * 0x10000 + c * 0x100 + d
+ s = cache[v0]
+ if not s then
+ local v = decoder[a] * 0x40000 + decoder[b] * 0x1000 + decoder[c] * 0x40 + decoder[d]
+ s = char(extract(v, 16, 8), extract(v, 8, 8), extract(v, 0, 8))
+ cache[v0] = s
+ end
+ else
+ local v = decoder[a] * 0x40000 + decoder[b] * 0x1000 + decoder[c] * 0x40 + decoder[d]
+ s = char(extract(v, 16, 8), extract(v, 8, 8), extract(v, 0, 8))
+ end
+ t[k] = s
+ k = k + 1
+ end
+ if padding == 1 then
+ local a, b, c = b64:byte(n - 3, n - 1)
+ local v = decoder[a] * 0x40000 + decoder[b] * 0x1000 + decoder[c] * 0x40
+ t[k] = char(extract(v, 16, 8), extract(v, 8, 8))
+ elseif padding == 2 then
+ local a, b = b64:byte(n - 3, n - 2)
+ local v = decoder[a] * 0x40000 + decoder[b] * 0x1000
+ t[k] = char(extract(v, 16, 8))
+ end
+ return concat(t)
+end
+
+return base64
+
+--[[
+------------------------------------------------------------------------------
+This software is available under 2 licenses -- choose whichever you prefer.
+------------------------------------------------------------------------------
+ALTERNATIVE A - MIT License
+Copyright (c) 2018 Ilya Kolbin
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+------------------------------------------------------------------------------
+ALTERNATIVE B - Public Domain (www.unlicense.org)
+This is free and unencumbered software released into the public domain.
+Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
+software, either in source code form or as a compiled binary, for any purpose,
+commercial or non-commercial, and by any means.
+In jurisdictions that recognize copyright laws, the author or authors of this
+software dedicate any and all copyright interest in the software to the public
+domain. We make this dedication for the benefit of the public at large and to
+the detriment of our heirs and successors. We intend this dedication to be an
+overt act of relinquishment in perpetuity of all present and future rights to
+this software under copyright law.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+------------------------------------------------------------------------------
+--]]
diff --git a/lua/fzf-lua/lib/serpent.lua b/lua/fzf-lua/lib/serpent.lua
new file mode 100644
index 00000000..502aab8d
--- /dev/null
+++ b/lua/fzf-lua/lib/serpent.lua
@@ -0,0 +1,158 @@
+--[[
+
+Source: https://github.com/pkulchenko/serpent
+
+--]]
+---@diagnostic disable
+local n, v = "serpent", "0.303" -- (C) 2012-18 Paul Kulchenko; MIT License
+local c, d = "Paul Kulchenko", "Lua serializer and pretty printer"
+local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'}
+local badtype = {thread = true, userdata = true, cdata = true}
+local getmetatable = debug and debug.getmetatable or getmetatable
+local pairs = function(t) return next, t end -- avoid using __pairs in Lua 5.2+
+local keyword, globals, G = {}, {}, (_G or _ENV)
+for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false',
+ 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat',
+ 'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end
+for k,v in pairs(G) do globals[v] = k end -- build func to name mapping
+for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do
+ for k,v in pairs(type(G[g]) == 'table' and G[g] or {}) do globals[v] = g..'.'..k end end
+
+local function s(t, opts)
+ local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum
+ local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge
+ local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge)
+ local maxlen, metatostring = tonumber(opts.maxlength), opts.metatostring
+ local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge)
+ local numformat = opts.numformat or "%.17g"
+ local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0
+ local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)",
+ -- tostring(val) is needed because __tostring may return a non-string value
+ function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end
+ local function safestr(s) return type(s) == "number" and (huge and snum[tostring(s)] or numformat:format(s))
+ or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026
+ or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end
+ -- handle radix changes in some locales
+ if opts.fixradix and (".1f"):format(1.2) ~= "1.2" then
+ local origsafestr = safestr
+ safestr = function(s) return type(s) == "number"
+ and (nohuge and snum[tostring(s)] or numformat:format(s):gsub(",",".")) or origsafestr(s)
+ end
+ end
+ local function comment(s,l) return comm and (l or 0) < comm and ' --[['..select(2, pcall(tostring, s))..']]' or '' end
+ local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal
+ and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end
+ local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r']
+ local n = name == nil and '' or name
+ local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n]
+ local safe = plain and n or '['..safestr(n)..']'
+ return (path or '')..(plain and path and '.' or '')..safe, safe end
+ local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding
+ local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'}
+ local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end
+ table.sort(k, function(a,b)
+ -- sort numeric keys first: k[key] is not nil for numerical keys
+ return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum))
+ < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end
+ local function val2str(t, name, indent, insref, path, plainindex, level)
+ local ttype, level, mt = type(t), (level or 0), getmetatable(t)
+ local spath, sname = safename(path, name)
+ local tag = plainindex and
+ ((type(name) == "number") and '' or name..space..'='..space) or
+ (name ~= nil and sname..space..'='..space or '')
+ if seen[t] then -- already seen this element
+ sref[#sref+1] = spath..space..'='..space..seen[t]
+ return tag..'nil'..comment('ref', level)
+ end
+ -- protect from those cases where __tostring may fail
+ if type(mt) == 'table' and metatostring ~= false then
+ local to, tr = pcall(function() return mt.__tostring(t) end)
+ local so, sr = pcall(function() return mt.__serialize(t) end)
+ if (to or so) then -- knows how to serialize itself
+ seen[t] = insref or spath
+ t = so and sr or tr
+ ttype = type(t)
+ end -- new value falls through to be serialized
+ end
+ if ttype == "table" then
+ if level >= maxl then return tag..'{}'..comment('maxlvl', level) end
+ seen[t] = insref or spath
+ if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty
+ if maxlen and maxlen < 0 then return tag..'{}'..comment('maxlen', level) end
+ local maxn, o, out = math.min(#t, maxnum or #t), {}, {}
+ for key = 1, maxn do o[key] = key end
+ if not maxnum or #o < maxnum then
+ local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables
+ for key in pairs(t) do
+ if o[key] ~= key then n = n + 1; o[n] = key end
+ end
+ end
+ if maxnum and #o > maxnum then o[maxnum+1] = nil end
+ if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end
+ local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output)
+ for n, key in ipairs(o) do
+ local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse
+ if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing
+ or opts.keyallow and not opts.keyallow[key]
+ or opts.keyignore and opts.keyignore[key]
+ or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types
+ or sparse and value == nil then -- skipping nils; do nothing
+ elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then
+ if not seen[key] and not globals[key] then
+ sref[#sref+1] = 'placeholder'
+ local sname = safename(iname, gensym(key)) -- iname is table for local variables
+ sref[#sref] = val2str(key,sname,indent,sname,iname,true)
+ end
+ sref[#sref+1] = 'placeholder'
+ local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']'
+ sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path))
+ else
+ out[#out+1] = val2str(value,key,indent,nil,seen[t],plainindex,level+1)
+ if maxlen then
+ maxlen = maxlen - #out[#out]
+ if maxlen < 0 then break end
+ end
+ end
+ end
+ local prefix = string.rep(indent or '', level)
+ local head = indent and '{\n'..prefix..indent or '{'
+ local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space))
+ local tail = indent and "\n"..prefix..'}' or '}'
+ return (custom and custom(tag,head,body,tail,level) or tag..head..body..tail)..comment(t, level)
+ elseif badtype[ttype] then
+ seen[t] = insref or spath
+ return tag..globerr(t, level)
+ elseif ttype == 'function' then
+ seen[t] = insref or spath
+ if opts.nocode then return tag.."function() --[[..skipped..]] end"..comment(t, level) end
+ local ok, res = pcall(string.dump, t)
+ local func = ok and "((loadstring or load)("..safestr(res)..",'@serialized'))"..comment(t, level)
+ return tag..(func or globerr(t, level))
+ else return tag..safestr(t) end -- handle all other types
+ end
+ local sepr = indent and "\n" or ";"..space
+ local body = val2str(t, name, indent) -- this call also populates sref
+ local tail = #sref>1 and table.concat(sref, sepr)..sepr or ''
+ local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or ''
+ return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end"
+end
+
+local function deserialize(data, opts)
+ local env = (opts and opts.safe == false) and G
+ or setmetatable({}, {
+ __index = function(t,k) return t end,
+ __call = function(t,...) error("cannot call functions") end
+ })
+ local f, res = (loadstring or load)('return '..data, nil, nil, env)
+ if not f then f, res = (loadstring or load)(data, nil, nil, env) end
+ if not f then return f, res end
+ if setfenv then setfenv(f, env) end
+ return pcall(f)
+end
+
+local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end
+return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s,
+ load = deserialize,
+ dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end,
+ line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end,
+ block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end }
diff --git a/lua/fzf-lua/libuv.lua b/lua/fzf-lua/libuv.lua
index 7b0f675a..9938498f 100644
--- a/lua/fzf-lua/libuv.lua
+++ b/lua/fzf-lua/libuv.lua
@@ -1,5 +1,7 @@
local uv = vim.loop
+local _is_win = vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1
+
local M = {}
-- path to current file
@@ -11,6 +13,9 @@ local __FILE__ = debug.getinfo(1, "S").source:gsub("^@", "")
-- effects (as 'vim.g.fzf_lua_directory=nil'). Run an additional
-- check if we are running headless with 'vim.api.nvim_list_uis'
if not vim.g.fzf_lua_directory and #vim.api.nvim_list_uis() == 0 then
+ -- global var indicating a headless instance
+ vim.g.fzf_lua_is_headless = true
+
-- prepend this folder first, so our modules always get first
-- priority over some unknown random module with the same name
package.path = (";%s/?.lua;"):format(vim.fn.fnamemodify(__FILE__, ":h"))
@@ -40,26 +45,41 @@ if not vim.g.fzf_lua_directory and #vim.api.nvim_list_uis() == 0 then
end
end
+local base64 = require("fzf-lua.lib.base64")
+local serpent = require("fzf-lua.lib.serpent")
+
-- save to upvalue for performance reasons
local string_byte = string.byte
local string_sub = string.sub
-local function find_last_newline(str)
+local function find_last(str, bytecode)
for i = #str, 1, -1 do
- if string_byte(str, i) == 10 then
+ if string_byte(str, i) == bytecode then
return i
end
end
end
-local function find_next_newline(str, start_idx)
+local function find_next(str, bytecode, start_idx)
+ local bytecodes = type(bytecode) == "table" and bytecode or { bytecode }
for i = start_idx or 1, #str do
- if string_byte(str, i) == 10 then
- return i
+ for _, b in ipairs(bytecodes) do
+ if string_byte(str, i) == b then
+ return i, string.char(b)
+ end
end
end
end
+local function find_last_newline(str)
+ return find_last(str, 10)
+end
+
+local function find_next_newline(str, start_idx)
+ return find_next(str, 10, start_idx)
+end
+
+
local function process_kill(pid, signal)
if not pid or not tonumber(pid) then return false end
if type(uv.os_getpriority(pid)) == "number" then
@@ -97,6 +117,9 @@ local function coroutinify(fn)
end
end
+---@param opts {cwd: string, cmd: string|table, env: table?, cb_finish: function, cb_write: function, cb_err: function, cb_pid: function, fn_transform: function?}
+---@param fn_transform function?
+---@param fn_done function?
M.spawn = function(opts, fn_transform, fn_done)
local output_pipe = uv.new_pipe(false)
local error_pipe = uv.new_pipe(false)
@@ -119,10 +142,24 @@ M.spawn = function(opts, fn_transform, fn_done)
-- https://github.com/luvit/luv/blob/master/docs.md
-- uv.spawn returns tuple: handle, pid
- local handle, pid = uv.spawn("sh", {
- args = { "-c", opts.cmd },
+ local shell = _is_win and "cmd" or "sh"
+ local args = _is_win and { "/d", "/e:off", "/f:off", "/v:on", "/c" } or { "-c" }
+ if type(opts.cmd) == "table" then
+ if _is_win then
+ ---@diagnostic disable-next-line: deprecated
+ table.move(opts.cmd, 1, #opts.cmd, #args + 1, args)
+ else
+ table.insert(args, table.concat(opts.cmd, " "))
+ end
+ else
+ table.insert(args, tostring(opts.cmd))
+ end
+ local handle, pid = uv.spawn(shell, {
+ args = args,
stdio = { nil, output_pipe, error_pipe },
- cwd = opts.cwd
+ cwd = opts.cwd,
+ env = opts.env,
+ verbatim = _is_win,
}, function(code, signal)
output_pipe:read_stop()
error_pipe:read_stop()
@@ -256,7 +293,9 @@ end
M.async_spawn = coroutinify(M.spawn)
-
+---@param opts {cmd: string, cwd: string, cb_pid: function, cb_finish: function, cb_write: function}
+---@param fn_transform function?
+---@param fn_preprocess function?
M.spawn_nvim_fzf_cmd = function(opts, fn_transform, fn_preprocess)
assert(not fn_transform or type(fn_transform) == "function")
@@ -292,7 +331,21 @@ M.spawn_nvim_fzf_cmd = function(opts, fn_transform, fn_preprocess)
end
end
-M.spawn_stdio = function(opts, fn_transform, fn_preprocess)
+---@param opts table
+---@param fn_transform_str string
+---@param fn_preprocess_str string
+M.spawn_stdio = function(opts, fn_transform_str, fn_preprocess_str)
+ -- attempt base64 decoding on all params
+ ---@param str string|table
+ ---@return string|table
+ local base64_conditional_decode = function(str)
+ if opts._base64 == false or type(str) ~= "string" then return str end
+ local ok, decoded = pcall(base64.decode, str)
+ return ok and decoded or str
+ end
+
+ ---@param fn_str string
+ ---@return function?
local function load_fn(fn_str)
if type(fn_str) ~= "string" then return end
local fn_loaded = nil
@@ -304,6 +357,17 @@ M.spawn_stdio = function(opts, fn_transform, fn_preprocess)
return fn_loaded
end
+ -- conditionally base64 decode, if not a base64 string, returns original value
+ opts = base64_conditional_decode(opts)
+ fn_transform_str = base64_conditional_decode(fn_transform_str)
+ fn_preprocess_str = base64_conditional_decode(fn_preprocess_str)
+
+ -- opts must be a table, if opts is a string deserialize
+ if type(opts) == "string" then
+ _, opts = serpent.load(opts)
+ assert(type(opts) == "table")
+ end
+
-- stdin/stdout are already buffered, not stderr. This means
-- that every character is flushed immedietely which caused
-- rendering issues on Mac (#316, #287) and Linux (#414)
@@ -318,14 +382,27 @@ M.spawn_stdio = function(opts, fn_transform, fn_preprocess)
opts.stderr_to_stdout = true
end
- fn_transform = load_fn(fn_transform)
- fn_preprocess = load_fn(fn_preprocess)
+ -- setup global vars
+ for k, v in pairs(opts.g or {}) do
+ _G[k] = v
+ if opts.debug == "v" or opts.debug == "verbose" then
+ io.stdout:write(string.format("[DEBUGV]: %s=%s\n", k, v))
+ end
+ end
+
+ local fn_transform = load_fn(fn_transform_str)
+ local fn_preprocess = load_fn(fn_preprocess_str)
-- run the preprocessing fn
if fn_preprocess then fn_preprocess(opts) end
-
- if opts.debug then
+ if opts.debug == "v" or opts.debug == "verbose" then
+ for k, v in pairs(opts) do
+ io.stdout:write(string.format("[DEBUGV]: %s=%s\n", k, v))
+ end
+ io.stdout:write(string.format("[DEBUGV]: fn_transform=%s\n", fn_transform_str))
+ io.stdout:write(string.format("[DEBUGV]: fn_preprocess=%s\n", fn_preprocess_str))
+ elseif opts.debug then
io.stdout:write("[DEBUG]: " .. opts.cmd .. "\n")
end
@@ -434,6 +511,19 @@ M.spawn_stdio = function(opts, fn_transform, fn_preprocess)
end)
end
+
+M.is_escaped = function(s, is_win)
+ local m
+ -- test spec override
+ if is_win == nil then is_win = _is_win end
+ if is_win then
+ m = s:match([[^".*"$]]) or s:match([[^%^".*%^"$]])
+ else
+ m = s:match([[^'.*'$]]) or s:match([[^".*"$]])
+ end
+ return m ~= nil
+end
+
-- our own version of vim.fn.shellescape compatibile with fish shells
-- * don't double-escape '\' (#340)
-- * if possible, replace surrounding single quote with double
@@ -441,9 +531,116 @@ end
-- If 'shell' contains "fish" in the tail, the "\" character will
-- be escaped because in fish it is used as an escape character
-- inside single quotes.
+--
+-- for windows, we assume we want to keep all quotes as literals
+-- to avoid the quotes being stripped when run from fzf actions
+-- we therefore have to escape the quotes with blackslashes and
+-- for nested quotes we double the blackslashes due to windows
+-- quirks, further reading:
+-- https://stackoverflow.com/questions/6714165/powershell-stripping-double-quotes-from-command-line-arguments
+-- https://learn.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
+--
-- this function is a better fit for utils but we're
-- trying to avoid having any 'require' in this file
-M.shellescape = function(s)
+M.shellescape = function(s, win_style)
+ if _is_win or win_style then
+ if tonumber(win_style) == 1 then
+ --
+ -- "classic" CommandLineToArgvW backslash escape
+ --
+ s = s:gsub([[\-"]], function(x)
+ -- Quotes found in string. From the above stackoverflow link:
+ --
+ -- (2n) + 1 backslashes followed by a quotation mark again produce n backslashes
+ -- followed by a quotation mark literal ("). This does not toggle the "in quotes"
+ -- mode.
+ --
+ -- to produce (2n)+1 backslashes we use the following `string.rep` calc:
+ -- (#x-1) * 2 + 1 - (#x-1) == #x
+ -- which translates to prepending the string with number of escape chars
+ -- (\) equal to its own length, this in turn is an **always odd** number
+ --
+ -- " -> \" (0->1)
+ -- \" -> \\\" (1->3)
+ -- \\" -> \\\\\" (2->5)
+ -- \\\" -> \\\\\\\" (3->7)
+ -- \\\\" -> \\\\\\\\\" (4->9)
+ --
+ x = string.rep([[\]], #x) .. x
+ return x
+ end)
+ s = s:gsub([[\+$]], function(x)
+ -- String ends with backslashes. From the above stackoverflow link:
+ --
+ -- 2n backslashes followed by a quotation mark again produce n backslashes
+ -- followed by a begin/end quote. This does not become part of the parsed
+ -- argument but toggles the "in quotes" mode.
+ --
+ -- c:\foo\ -> "c:\foo\" // WRONG
+ -- c:\foo\ -> "c:\foo\\" // RIGHT
+ -- c:\foo\\ -> "c:\foo\\" // WRONG
+ -- c:\foo\\ -> "c:\foo\\\\" // RIGHT
+ --
+ -- To produce equal number of backslashes without converting the ending quote
+ -- to a quote literal, double the backslashes (2n), **always even** number
+ x = string.rep([[\]], #x * 2)
+ return x
+ end)
+ return [["]] .. s .. [["]]
+ else
+ --
+ -- CMD.exe caret+backslash escape, after lot of trial and error
+ -- this seems to be the winning logic, a combination of v1 above
+ -- and caret escaping special chars
+ --
+ -- The logic is as follows
+ -- (1) all escaped quotes end up the same \^"
+ -- (1) if quote was prepended with backslash or backslash+caret
+ -- the resulting number of backslashes will be 2n + 1
+ -- (2) if caret exists between the backslash/quote combo, move it
+ -- before the backslash(s)
+ -- (4) all cmd special chars are escaped with ^
+ --
+ -- NOTE: explore "tests/libuv_spec.lua" to see examples of quoted
+ -- combinations and their expecetd results
+ --
+ local escape_inner = function(inner)
+ inner = inner:gsub([[\-%^?"]], function(x)
+ -- although we currently only transfer 1 caret, the below
+ -- can handle any number of carets with the regex [[\-%^-"]]
+ local carets = x:match("%^+") or ""
+ x = carets .. string.rep([[\]], #x - #(carets)) .. x:gsub("%^+", "")
+ return x
+ end)
+ -- escape all windows metacharacters but quotes
+ -- ( ) % ! ^ < > & | "
+ -- TODO: should % be escaped with ^ or %?
+ inner = inner:gsub('[%(%)%%!%^<>&|%s"]', function(x)
+ return "^" .. x
+ end)
+ -- escape backslashes at the end of the string
+ inner = inner:gsub([[\+$]], function(x)
+ x = string.rep([[\]], #x * 2)
+ return x
+ end)
+ return inner
+ end
+ s = escape_inner(s)
+ if s:match("!") and tonumber(win_style) == 2 then
+ --
+ -- https://ss64.com/nt/syntax-esc.html
+ -- This changes slightly if you are running with DelayedExpansion of variables:
+ -- if any part of the command line includes an '!' then CMD will escape a second
+ -- time, so ^^^^ will become ^
+ --
+ -- NOTE: we only do this on demand (currently only used in "libuv_spec.lua")
+ --
+ s = escape_inner(s)
+ end
+ s = [[^"]] .. s .. [[^"]]
+ return s
+ end
+ end
local shell = vim.o.shell
if not shell or not shell:match("fish$") then
return vim.fn.shellescape(s)
@@ -469,23 +666,58 @@ M.shellescape = function(s)
end
end
+-- Windows fzf oddities, fzf's {q} will send escaped blackslahes,
+-- but only when the backslash prefixes another character which
+-- isn't a backslash
+M.unescape_fzf = function(s, is_win)
+ if is_win == nil then is_win = _is_win end
+ if not is_win then return s end
+ local ret = s:gsub("\\+[^\\]", function(x)
+ local bslash_num = #x:match([[\+]])
+ return string.rep([[\]],
+ bslash_num == 1 and bslash_num or bslash_num / 2) .. x:sub(-1)
+ end)
+ return ret
+end
+
+-- with live_grep, we use a modified "reload" command as our
+-- FZF_DEFAULT_COMMAND and due to the above oddity with fzf
+-- doing weird extra escaping with {q}, we use this to simulate
+-- {q} being sent via the reload action as the initial command
+-- TODO: better solution for these stupid hacks (upstream issues?)
+M.escape_fzf = function(s, is_win)
+ if is_win == nil then is_win = _is_win end
+ if not is_win then return s end
+ local ret = s:gsub("\\+[^\\]", function(x)
+ local bslash_num = #x:match([[\+]])
+ return string.rep([[\]], bslash_num * 2) .. x:sub(-1)
+ end)
+ return ret
+end
+
+---@param opts string
+---@param fn_transform string?
+---@param fn_preprocess string?
+---@return string
M.wrap_spawn_stdio = function(opts, fn_transform, fn_preprocess)
assert(opts and type(opts) == "string")
assert(not fn_transform or type(fn_transform) == "string")
local nvim_bin = os.getenv("FZF_LUA_NVIM_BIN") or vim.v.progpath
local nvim_runtime = os.getenv("FZF_LUA_NVIM_BIN") and ""
- or string.format("VIMRUNTIME=%s ", M.shellescape(vim.env.VIMRUNTIME))
- local call_args = opts
- for _, fn in ipairs({ fn_transform, fn_preprocess }) do
- if type(fn) == "string" then
- call_args = ("%s,[[%s]]"):format(call_args, fn)
- end
- end
+ or string.format(
+ _is_win and [[set VIMRUNTIME=%s& ]] or "VIMRUNTIME=%s ",
+ _is_win and vim.fs.normalize(vim.env.VIMRUNTIME) or M.shellescape(vim.env.VIMRUNTIME)
+ )
+ local lua_cmd = ("lua vim.g.did_load_filetypes=1; loadfile([[%s]])().spawn_stdio(%s,%s,%s)")
+ :format(
+ _is_win and vim.fs.normalize(__FILE__) or __FILE__,
+ opts, fn_transform, fn_preprocess
+ )
local cmd_str = ("%s%s -n --headless --clean --cmd %s"):format(
nvim_runtime,
- M.shellescape(nvim_bin),
- M.shellescape(("lua loadfile([[%s]])().spawn_stdio(%s)")
- :format(__FILE__, call_args)))
+ M.shellescape(_is_win and vim.fs.normalize(nvim_bin) or nvim_bin),
+ M.shellescape(lua_cmd)
+ )
return cmd_str
end
diff --git a/lua/fzf-lua/make_entry.lua b/lua/fzf-lua/make_entry.lua
index bddccfc1..c01b40a6 100644
--- a/lua/fzf-lua/make_entry.lua
+++ b/lua/fzf-lua/make_entry.lua
@@ -2,6 +2,8 @@ local M = {}
local path = require "fzf-lua.path"
local utils = require "fzf-lua.utils"
+local libuv = require "fzf-lua.libuv"
+local devicons = require "fzf-lua.devicons"
local config = nil
-- attempt to load the current config
@@ -11,19 +13,12 @@ do
if ok then config = module end
end
--- These globals are set by spawn.fn_transform loadstring
----@diagnostic disable-next-line: undefined-field
-M._fzf_lua_server = _G._fzf_lua_server
----@diagnostic disable-next-line: undefined-field
-M._devicons_path = _G._devicons_path
----@diagnostic disable-next-line: undefined-field
-M._devicons_setup = _G._devicons_setup
-
local function load_config_section(s, datatype, optional)
if config then
local val = utils.map_get(config, s)
return type(val) == datatype and val or nil
- elseif M._fzf_lua_server then
+ ---@diagnostic disable-next-line: undefined-field
+ elseif _G._fzf_lua_server then
-- load config from our running instance
local res = nil
local is_bytecode = false
@@ -37,7 +32,8 @@ local function load_config_section(s, datatype, optional)
exec_str = ("return require'fzf-lua'.config.%s"):format(s)
end
local ok, errmsg = pcall(function()
- local chan_id = vim.fn.sockconnect("pipe", M._fzf_lua_server, { rpc = true })
+ ---@diagnostic disable-next-line: undefined-field
+ local chan_id = vim.fn.sockconnect("pipe", _G._fzf_lua_server, { rpc = true })
res = vim.rpcrequest(chan_id, "nvim_exec_lua", exec_str, exec_opts)
vim.fn.chanclose(chan_id)
end)
@@ -53,146 +49,12 @@ local function load_config_section(s, datatype, optional)
end
end
--- NOT NEEDED SINCE RESUME DATA REFACTOR
--- local function set_config_section(s, data)
--- if M._fzf_lua_server then
--- -- save config in our running instance
--- local ok, errmsg = pcall(function()
--- local chan_id = vim.fn.sockconnect("pipe", M._fzf_lua_server, { rpc = true })
--- vim.rpcrequest(chan_id, "nvim_exec_lua", ([[
--- local data = select(1, ...)
--- require'fzf-lua'.config.%s = data
--- ]]):format(s), { data })
--- vim.fn.chanclose(chan_id)
--- end)
--- if not ok then
--- io.stderr:write(("Error setting remote config section '%s': %s\n")
--- :format(s, errmsg))
--- end
--- return ok
--- elseif config then
--- local keys = utils.strsplit(s, ".")
--- local iter = config
--- for i = 1, #keys do
--- iter = iter[keys[i]]
--- if not iter then break end
--- if i == #keys - 1 then
--- iter[keys[i + 1]] = data
--- return iter
--- end
--- end
--- end
--- end
-
--- Setup the terminal colors codes for nvim-web-devicons colors
-M.setup_devicon_term_hls = function()
- if M.__HL_BG and vim.o.bg == M.__HL_BG then
- -- already setup for the current `bg`, do nothing (#893)
- -- this was already taken care of for multiprocess in
- -- `config._devicons_geticons` in #855
- return
- end
- local icons = M._devicons and M._devicons.get_icons() or M._devicons_map
- if not icons then
- return
- end
- -- save the current neovim background
- M.__HL_BG = vim.o.bg
-
- local function hex(hexstr)
- local r, g, b = hexstr:match(".(..)(..)(..)")
- r, g, b = tonumber(r, 16), tonumber(g, 16), tonumber(b, 16)
- return r, g, b
- end
-
- for k, info in pairs(icons) do
- -- info.name can be missing (#817)
- local name = info.name or type(k) == "string" and k
- if name then
- local hlgroup = "DevIcon" .. name
- -- some devicons customizations remove `info.color`
- -- retrieve the color from the highlight group (#801)
- local hexcol = info.color or utils.hexcol_from_hl(hlgroup, "fg")
- if hexcol and #hexcol > 0 then
- local r, g, b = hex(hexcol)
- utils.cache_ansi_escseq(hlgroup, string.format("[38;2;%s;%s;%sm", r, g, b))
- end
- end
- end
-end
-
--- cache directory icon coloring escape sequence
-M.__DIR_ICON = nil
-M.__DIR_ICON_HL = "FzfLuaDirIcon"
-
-M.setup_directory_icon = function()
- M.__DIR_ICON = config.globals.dir_icon
- -- `M._diricon_escseq` cab be nil if hlgroup is cleared or non-existent
- local escseq = M._diricon_escseq or config._diricon_escseq and config._diricon_escseq()
- utils.cache_ansi_escseq(M.__DIR_ICON_HL, escseq)
-end
-
-local function load_devicons()
- if config and config._has_devicons then
- -- file was called from the primary instance
- -- acquire nvim-web-devicons from config
- M._devicons = config._devicons
- elseif M._fzf_lua_server and load_config_section("_has_devicons", "boolean") then
- -- file was called from a headless instance
- -- load nvim-web-devicons via the RPC to the main instance
- M._devicons_map = load_config_section("_devicons_geticons()", "table")
- M._diricon_escseq = load_config_section("_diricon_escseq()", "string")
- end
- if not M._devicons and not M._devicons_map
- and M._devicons_path and vim.loop.fs_stat(M._devicons_path) then
- -- file was called from a headless instance
- -- fallback load nvim-web-devicons manually
- -- add nvim-web-devicons path to `package.path`
- -- so `require("nvim-web-devicons")` can find it
- package.path = (";%s/?.lua;"):format(vim.fn.fnamemodify(M._devicons_path, ":h"))
- .. package.path
- M._devicons = require("nvim-web-devicons")
- -- WE NO LONGER USE THIS, LEFT FOR DOCUMENTATION
- -- loading with 'require' is needed, 'loadfile'
- -- cannot load a custom setup function as it's
- -- considered a separate instance and the inner
- -- 'require' in the setup file will create an
- -- additional 'nvim-web-devicons' instance
- --[[ local file = loadfile(M._devicons_path)
- M._devicons = file and file() ]]
- -- did caller specify a custom setup function?
- -- must be called before the next step as `setup`
- -- is ignored when called the second time
- M._devicons_setup = M._devicons_setup and vim.fn.expand(M._devicons_setup)
- if M._devicons and M._devicons_setup and vim.loop.fs_stat(M._devicons_setup) then
- local file = loadfile(M._devicons_setup)
- if file then file() end
- end
- end
- if M._devicons and M._devicons.setup and not M._devicons.has_loaded() then
- -- if the caller has devicons lazy loaded
- -- running without calling setup will generate an error:
- -- nvim-web-devicons.lua:972: E5560:
- -- nvim_command must not be called in a lua loop callback
- -- running in a pcall to avoid panic with neovim <= 0.6
- -- due to usage of new highlighting API introduced with v0.7
- pcall(M._devicons.setup)
- end
- -- Setup devicon terminal ansi color codes
- M.setup_devicon_term_hls()
- M.setup_directory_icon()
-end
-
--- Load remote config and devicons
-pcall(load_devicons)
-
if not config then
local _config = { globals = { git = {}, files = {}, grep = {} } }
_config.globals.git.icons = load_config_section("globals.git.icons", "table") or {}
- _config.globals.dir_icon = load_config_section("globals.dir_icon", "string")
- _config.globals.file_icon_colors = load_config_section("globals.file_icon_colors", "table") or {}
- _config.globals.file_icon_padding = load_config_section("globals.file_icon_padding", "string")
- _config.globals.files.git_status_cmd = load_config_section("globals.files.git_status_cmd", "table")
+ _config.globals.files.git_status_cmd =
+ load_config_section("globals.files.git_status_cmd", "table")
+ or { "git", "-c", "color.status=false", "--no-optional-locks", "status", "--porcelain=v1" }
-- prioritize `opts.rg_glob_fn` over globals
_config.globals.grep.rg_glob_fn =
@@ -205,38 +67,6 @@ if not config then
config = _config
end
-M.get_devicon = function(file, ext)
- local icon, hl
- if path.ends_with_separator(file) then
- icon, hl = M.__DIR_ICON, M.__DIR_ICON_HL
- elseif M._devicons then
- icon, hl = M._devicons.get_icon(file, ext:lower(), { default = true })
- elseif M._devicons_map then
- -- Lookup first by name, then by ext (devicons `strict=true`)
- -- "" is added by fzf-lua and is thus guaranteed
- local info = M._devicons_map[file:lower()]
- or M._devicons_map[ext:lower()]
- or M._devicons_map[""]
- icon, hl = info.icon, "DevIcon" .. info.name
- else
- icon, hl = "ļ¶", "dark_grey"
- end
-
- -- allow user override of the color
- local override = config.globals.file_icon_colors
- and config.globals.file_icon_colors[ext]
- if override then
- hl = override
- end
-
- if config.globals.file_icon_padding and
- #config.globals.file_icon_padding > 0 then
- icon = icon .. config.globals.file_icon_padding
- end
-
- return icon, hl
-end
-
M.get_diff_files = function(opts)
local diff_files = {}
local cmd = opts.git_status_cmd or config.globals.files.git_status_cmd
@@ -259,6 +89,10 @@ M.get_diff_files = function(opts)
return diff_files
end
+---@param query string
+---@param opts table
+---@return string search_query
+---@return string? glob_args
M.glob_parse = function(query, opts)
if not query or not query:find(opts.glob_separator) then
return query, nil
@@ -270,14 +104,17 @@ M.glob_parse = function(query, opts)
local glob_args = ""
local search_query, glob_str = query:match("(.*)" .. opts.glob_separator .. "(.*)")
for _, s in ipairs(utils.strsplit(glob_str, "%s")) do
- glob_args = glob_args .. ("%s %s ")
- :format(opts.glob_flag, vim.fn.shellescape(s))
+ glob_args = glob_args .. ("%s %s "):format(opts.glob_flag, libuv.shellescape(s))
end
return search_query, glob_args
end
-- reposition args before ` -e ` or ` -- `
-- enables "-e" and "--fixed-strings --" in `rg_opts` (#781, #794)
+---@param cmd string
+---@param args string
+---@param relocate_pattern string?
+---@return string
M.rg_insert_args = function(cmd, args, relocate_pattern)
local patterns = {}
for _, a in ipairs({
@@ -310,48 +147,53 @@ M.rg_insert_args = function(cmd, args, relocate_pattern)
end
M.preprocess = function(opts)
- if opts.cwd_only and not opts.cwd then
- opts.cwd = vim.loop.cwd()
- end
-
- if opts.file_icons then
- -- refersh the directory icon hlgroup
- M.setup_directory_icon()
- end
-
- if opts.git_icons then
- opts.diff_files = M.get_diff_files(opts)
- end
-
local argv = function(i, debug)
-- argv1 is actually the 7th argument if we count
-- arguments already supplied by 'wrap_spawn_stdio'.
-- If no index was supplied use the last argument
local idx = tonumber(i) and tonumber(i) + 6 or #vim.v.argv
- if debug then
- io.stdout:write(("[DEBUG]: argv(%d) = %s\n")
- :format(idx, vim.fn.shellescape(vim.v.argv[idx])))
+ local arg = vim.v.argv[idx]
+ if debug == "v" or debug == "verbose" then
+ io.stdout:write(("[DEBUGV]: raw_argv(%d) = %s\n"):format(idx, arg))
+ end
+ if utils.__IS_WINDOWS then
+ arg = libuv.unescape_fzf(arg)
end
- return vim.v.argv[idx]
+ if debug == "v" or debug == "verbose" then
+ io.stdout:write(("[DEBUGV]: esc_argv(%d) = %s\n"):format(idx, libuv.shellescape(arg)))
+ end
+ return arg
end
-- live_grep replace pattern with last argument
local argvz = "{argvz}"
- local has_argvz = opts.cmd and opts.cmd:match(argvz)
+ if opts.cmd and opts.cmd:match(argvz) then
+ -- The NEQ condition on Windows turned out to be a real pain in the butt
+ -- so I decided to move the empty query test into our cmd proxy wrapper
+ -- For obvious reasons this cannot work with `live_grep_native` and thus
+ -- the NEQ condition remains for the "native" version
+ if not opts.exec_empty_query then
+ -- query is always be the last argument
+ if argv(nil, opts.debug) == "" then
+ opts.cmd = utils.shell_nop()
+ return opts
+ end
+ end
- -- did the caller request rg with glob support?
- -- manipulation needs to be done before the argv hack
- if opts.rg_glob and has_argvz then
- local query = argv()
- local search_query, glob_args = M.glob_parse(query, opts)
- if glob_args then
- -- gsub doesn't like single % on rhs
- search_query = search_query:gsub("%%", "%%%%")
- -- reset argvz so it doesn't get replaced again below
- -- insert glob args before `-- {argvz}` or `-e {argvz}` repositioned
- -- at the end of the command preceding the search query (#781, #794)
- opts.cmd = M.rg_insert_args(opts.cmd, glob_args, argvz)
- opts.cmd = opts.cmd:gsub(argvz, vim.fn.shellescape(search_query))
+ -- did the caller request rg with glob support?
+ -- manipulation needs to be done before the argv replacement
+ if opts.rg_glob then
+ local query = argv(nil, opts.debug)
+ local search_query, glob_args = M.glob_parse(query, opts)
+ if glob_args then
+ -- gsub doesn't like single % on rhs
+ search_query = search_query:gsub("%%", "%%%%")
+ -- reset argvz so it doesn't get replaced again below
+ -- insert glob args before `-- {argvz}` or `-e {argvz}` repositioned
+ -- at the end of the command preceding the search query (#781, #794)
+ opts.cmd = M.rg_insert_args(opts.cmd, glob_args, argvz)
+ opts.cmd = opts.cmd:gsub(argvz, libuv.shellescape(search_query))
+ end
end
end
@@ -361,10 +203,44 @@ M.preprocess = function(opts)
opts.cmd = opts.cmd:gsub("{argv.*}",
function(x)
local idx = x:match("{argv(.*)}")
- return vim.fn.shellescape(argv(idx))
+ return libuv.shellescape(argv(idx, not opts.rg_glob and opts.debug))
end)
end
+ if utils.__IS_WINDOWS and opts.cmd:match("!") then
+ -- https://ss64.com/nt/syntax-esc.html
+ -- This changes slightly if you are running with DelayedExpansion of variables:
+ -- if any part of the command line includes an '!' then CMD will escape a second
+ -- time, so ^^^^ will become ^
+ -- replace in sections, only double the relevant pipe sections with !
+ local escaped_cmd = {}
+ for _, str in ipairs(utils.strsplit(opts.cmd, "%s+|")) do
+ if str:match("!") then
+ str = str:gsub('[%(%)%%!%^<>&|"]', function(x)
+ return "^" .. x
+ end)
+ -- make sure all ! are escaped at least twice
+ str = str:gsub("[^%^]%^!", function(x)
+ return x:sub(1, 1) .. "^" .. x:sub(2)
+ end)
+ end
+ table.insert(escaped_cmd, str)
+ end
+ opts.cmd = table.concat(escaped_cmd, " |")
+ end
+
+ if opts.cwd_only and not opts.cwd then
+ opts.cwd = vim.loop.cwd()
+ end
+
+ if opts.file_icons then
+ devicons.load()
+ end
+
+ if opts.git_icons then
+ opts.diff_files = M.get_diff_files(opts)
+ end
+
return opts
end
@@ -384,11 +260,25 @@ end
local COLON_BYTE = string.byte(":")
+---@param x string
+---@param opts table
+---@return string? entry
M.file = function(x, opts)
opts = opts or {}
local ret = {}
local icon, hl
- local colon_idx = utils.find_next_char(x, COLON_BYTE) or 0
+ local colon_start_idx = 1
+ if utils.__IS_WINDOWS then
+ if string.byte(x, #x) == 13 then
+ -- strip ^M added by the "dir /s/b" command
+ x = x:sub(1, #x - 1)
+ end
+ if path.is_absolute(x) then
+ -- ignore the first colon in the drive spec, e.g c:\
+ colon_start_idx = 3
+ end
+ end
+ local colon_idx = utils.find_next_char(x, COLON_BYTE, colon_start_idx) or 0
local file_part = colon_idx > 1 and x:sub(1, colon_idx - 1) or x
local rest_of_line = colon_idx > 1 and x:sub(colon_idx) or nil
-- strip ansi coloring from path so we can use filters
@@ -401,18 +291,18 @@ M.file = function(x, opts)
-- fd v8.3 requires adding '--strip-cwd-prefix' to remove
-- the './' prefix, will not work with '--color=always'
-- https://github.com/sharkdp/fd/blob/master/CHANGELOG.md
- if not (opts.strip_cwd_prefix == false) and path.starts_with_cwd(filepath) then
+ if not (opts.strip_cwd_prefix == false) then
filepath = path.strip_cwd_prefix(filepath)
end
-- make path relative
if opts.cwd and #opts.cwd > 0 then
- filepath = path.relative(filepath, opts.cwd)
+ filepath = path.relative_to(filepath, opts.cwd)
end
- if path.starts_with_separator(filepath) then
+ if path.is_absolute(filepath) then
-- filter for cwd only
if opts.cwd_only then
local cwd = opts.cwd or vim.loop.cwd()
- if not path.is_relative(filepath, cwd) then
+ if not path.is_relative_to(filepath, cwd) then
return nil
end
end
@@ -432,10 +322,15 @@ M.file = function(x, opts)
-- save a copy for git indicator and icon lookups
local origpath = filepath
if opts.path_shorten then
- filepath = path.shorten(filepath, tonumber(opts.path_shorten))
+ filepath = path.shorten(filepath, tonumber(opts.path_shorten),
+ -- On Windows we want to shorten using the separator used by the `cwd` arg
+ -- otherwise we might haave issues "lenghening" as in the case of git which
+ -- uses normalized paths (using /) for `rev-parse --show-toplevel` and `ls-files`
+ utils.__IS_WINDOWS and opts.cwd and path.separator(opts.cwd))
end
if opts.git_icons then
- local diff_info = opts.diff_files and opts.diff_files[origpath]
+ local diff_info = opts.diff_files
+ and opts.diff_files[utils._if_win(path.normalize(origpath), origpath)]
local indicators = diff_info and diff_info[1] or utils.nbsp
for i = 1, #indicators do
icon = indicators:sub(i, i)
@@ -453,14 +348,9 @@ M.file = function(x, opts)
ret[#ret + 1] = utils.nbsp
end
if opts.file_icons then
- local filename = path.tail(origpath)
- local ext = path.extension(filename)
- icon, hl = M.get_devicon(filename, ext)
- if opts.color_icons then
- -- extra workaround for issue #119 (or similars)
- -- use default if we can't find the highlight ansi
- local fn = utils.ansi_codes[hl] or utils.ansi_codes["dark_grey"]
- icon = fn(icon)
+ icon, hl = devicons.get_devicon(origpath)
+ if hl and opts.color_icons then
+ icon = utils.ansi_from_rgb(hl, icon)
end
ret[#ret + 1] = icon
ret[#ret + 1] = utils.nbsp
diff --git a/lua/fzf-lua/path.lua b/lua/fzf-lua/path.lua
index 059e2fa7..2369980e 100644
--- a/lua/fzf-lua/path.lua
+++ b/lua/fzf-lua/path.lua
@@ -1,132 +1,231 @@
local utils = require "fzf-lua.utils"
+local libuv = require "fzf-lua.libuv"
local string_sub = string.sub
local string_byte = string.byte
local M = {}
-M.SEPARATOR = "/"
+M.dot_byte = string_byte(".")
+M.colon_byte = string_byte(":")
+M.fslash_byte = string_byte("/")
+M.bslash_byte = string_byte([[\]])
-M.separator = function()
- return M.SEPARATOR
+---@param path string?
+---@return string
+M.separator = function(path)
+ -- auto-detect separator from fully qualified paths, e.g. "C:\..." or "~/..."
+ if utils.__IS_WINDOWS and path then
+ local maybe_separators = { string_byte(path, 3), string_byte(path, 2) }
+ for _, s in ipairs(maybe_separators) do
+ if M.byte_is_separator(s) then
+ return string.char(s)
+ end
+ end
+ end
+ return string.char(utils._if_win(M.bslash_byte, M.fslash_byte))
end
-M.dot_byte = string_byte(".")
-M.separator_byte = string_byte(M.SEPARATOR)
-
-M.starts_with_separator = function(path)
- return string_byte(path, 1) == M.separator_byte
+M.separator_byte = function(path)
+ return string_byte(M.separator(path), 1)
end
-M.ends_with_separator = function(path)
- return string_byte(path, #path) == M.separator_byte
+---@param byte number
+---@return boolean
+M.byte_is_separator = function(byte)
+ if utils.__IS_WINDOWS then
+ -- path on windows can also be the result of `vim.fs.normalize`
+ -- so we need to test for the presense of both slash types
+ return byte == M.bslash_byte or byte == M.fslash_byte
+ else
+ return byte == M.fslash_byte
+ end
end
-M.starts_with_cwd = function(path)
- return #path > 1
- and string_byte(path, 1) == M.dot_byte
- and string_byte(path, 2) == M.separator_byte
- -- return path:match("^."..M.SEPARATOR) ~= nil
+M.is_separator = function(c)
+ return M.byte_is_separator(string_byte(c, 1))
end
-M.strip_cwd_prefix = function(path)
- return #path > 2 and path:sub(3)
+---@param path string
+---@return boolean
+M.ends_with_separator = function(path)
+ return M.byte_is_separator(string_byte(path, #path))
end
-function M.tail(path)
- local end_idx = M.ends_with_separator(path) and (#path - 1) or #path
- for i = end_idx, 1, -1 do
- if string_byte(path, i) == M.separator_byte then
- return path:sub(i + 1)
- end
+---@param path string
+---@return string
+function M.add_trailing(path)
+ if M.ends_with_separator(path) then
+ return path
end
- return path
+ return path .. M.separator(path)
end
-function M.extension(path)
- for i = #path, 1, -1 do
- if string_byte(path, i) == 46 then
- return path:sub(i + 1)
- end
+---@param path string
+---@return string
+function M.remove_trailing(path)
+ while M.ends_with_separator(path) do
+ path = path:sub(1, #path - 1)
end
return path
end
-function M.to_matching_str(path)
- -- return path:gsub('(%-)', '(%%-)'):gsub('(%.)', '(%%.)'):gsub('(%_)', '(%%_)')
- -- above is missing other lua special chars like '+' etc (#315)
- return utils.lua_regex_escape(path)
+---@param path string
+---@return boolean
+M.is_absolute = function(path)
+ return utils._if_win(
+ string_byte(path, 2) == M.colon_byte,
+ string_byte(path, 1) == M.fslash_byte)
end
-function M.join(paths)
- -- gsub to remove double separator
- local ret = table.concat(paths, M.SEPARATOR):gsub(M.SEPARATOR .. M.SEPARATOR, M.SEPARATOR)
- return ret
+---@param path string
+---@return boolean
+M.has_cwd_prefix = function(path)
+ return #path > 1
+ and string_byte(path, 1) == M.dot_byte
+ and M.byte_is_separator(string_byte(path, 2))
end
-function M.split(path)
- return path:gmatch("[^" .. M.SEPARATOR .. "]+" .. M.SEPARATOR .. "?")
+---@param path string
+---@return string
+M.strip_cwd_prefix = function(path)
+ if M.has_cwd_prefix(path) then
+ return #path > 2 and path:sub(3) or ""
+ else
+ return path
+ end
end
----Get the basename of the given path.
+---Get the basename|tail of the given path.
---@param path string
---@return string
-function M.basename(path)
- path = M.remove_trailing(path)
- local i = path:match("^.*()" .. M.SEPARATOR)
- if not i then return path end
- return path:sub(i + 1, #path)
+function M.tail(path)
+ local end_idx = M.ends_with_separator(path) and #path - 1 or #path
+ for i = end_idx, 1, -1 do
+ if M.byte_is_separator(string_byte(path, i)) then
+ return path:sub(i + 1)
+ end
+ end
+ return path
end
----Get the path to the parent directory of the given path. Returns `nil` if the
----path has no parent.
+M.basename = M.tail
+
+---Get the path to the parent directory of the given path.
+-- Returns `nil` if the path has no parent.
---@param path string
---@param remove_trailing boolean
----@return string|nil
+---@return string?
function M.parent(path, remove_trailing)
- path = " " .. M.remove_trailing(path)
- local i = path:match("^.+()" .. M.SEPARATOR)
- if not i then return nil end
- path = path:sub(2, i)
- if remove_trailing then
- path = M.remove_trailing(path)
+ path = M.remove_trailing(path)
+ for i = #path, 1, -1 do
+ if M.byte_is_separator(string_byte(path, i)) then
+ local parent = path:sub(1, i)
+ if remove_trailing then
+ parent = M.remove_trailing(parent)
+ end
+ return parent
+ end
end
- return path
end
----Get a path relative to another path.
---@param path string
----@param relative_to string
---@return string
-function M.relative(path, relative_to)
- local p, _ = path:gsub("^" .. M.to_matching_str(M.add_trailing(relative_to)), "")
+function M.normalize(path)
+ local p = M.tilde_to_HOME(path)
+ if utils.__IS_WINDOWS then
+ p = p:gsub([[\]], [[/]])
+ end
return p
end
-function M.is_relative(path, relative_to)
- local p = path:match("^" .. M.to_matching_str(M.add_trailing(relative_to)))
- return p ~= nil
+---@param p1 string
+---@param p2 string
+---@return boolean
+function M.equals(p1, p2)
+ p1 = M.normalize(M.remove_trailing(p1))
+ p2 = M.normalize(M.remove_trailing(p2))
+ if utils.__IS_WINDOWS then
+ p1 = string.lower(p1)
+ p2 = string.lower(p2)
+ end
+ return p1 == p2
end
-function M.add_trailing(path)
- if path:sub(-1) == M.SEPARATOR then
- return path
- end
+---@param path string
+---@param relative_to string
+---@return boolean, string?
+function M.is_relative_to(path, relative_to)
+ -- make sure paths end with a separator
+ local path_no_trailing = M.tilde_to_HOME(path)
+ path = M.add_trailing(path_no_trailing)
+ relative_to = M.add_trailing(M.tilde_to_HOME(relative_to))
+ local pidx, ridx = 1, 1
+ repeat
+ local pbyte = string.byte(path, pidx)
+ local rbyte = string.byte(relative_to, ridx)
+ if M.byte_is_separator(pbyte) and M.byte_is_separator(rbyte) then
+ -- both path and relative_to have a separator part
+ -- which may differ in length if there are multiple
+ -- separators, e.g. "/some/path" and "//some//path"
+ repeat
+ pidx = pidx + 1
+ until not M.byte_is_separator(string.byte(path, pidx))
+ repeat
+ ridx = ridx + 1
+ until not M.byte_is_separator(string.byte(relative_to, ridx))
+ elseif utils.__IS_WINDOWS and pbyte and rbyte
+ -- case insensitive matching on windows
+ and string.char(pbyte):lower() == string.char(rbyte):lower()
+ -- byte matching on Unix/BSD
+ or pbyte == rbyte then
+ -- character matches, move to next
+ pidx = pidx + 1
+ ridx = ridx + 1
+ else
+ -- characters don't match
+ return false, nil
+ end
+ until ridx > #relative_to
+ return true, pidx <= #path_no_trailing and path_no_trailing:sub(pidx) or "."
+end
- return path .. M.SEPARATOR
+---Get a path relative to another path.
+---@param path string
+---@param relative_to string
+---@return string
+function M.relative_to(path, relative_to)
+ local is_relative_to, relative_path = M.is_relative_to(path, relative_to)
+ return is_relative_to and relative_path or path
end
-function M.remove_trailing(path)
- local p, _ = path:gsub(M.SEPARATOR .. "$", "")
- return p
+---@param path string
+---@return string?
+function M.extension(path, no_tail)
+ local file = no_tail and path or M.tail(path)
+ for i = #file, 1, -1 do
+ if string_byte(file, i) == M.dot_byte then
+ return file:sub(i + 1)
+ end
+ end
end
-local function find_next(str, char, start_idx)
- local i_char = string_byte(char, 1)
- for i = start_idx or 1, #str do
- if string_byte(str, i) == i_char then
- return i
+---@param paths string[]
+---@return string
+function M.join(paths)
+ -- Separator is always / (even on windows) unless we
+ -- detect it from fully qualified paths, e.g. "C:\..."
+ local separator = M.separator(paths[1])
+ local ret = ""
+ for i = 1, #paths do
+ local p = paths[i]
+ if p then
+ if i < #paths and not M.ends_with_separator(p) then
+ p = p .. separator
+ end
+ ret = ret .. p
end
end
+ return ret
end
-- I'm not sure why this happens given that neovim is single threaded
@@ -137,27 +236,58 @@ M.HOME = function()
if not M.__HOME then
-- use 'os.getenv' instead of 'vim.env' due to (#452):
-- E5560: nvim_exec must not be called in a lua loop callback
- -- M.__HOME = vim.env.HOME
- M.__HOME = os.getenv("HOME")
+ M.__HOME = utils._if_win(os.getenv("USERPROFILE"), os.getenv("HOME"))
end
return M.__HOME
end
+---@param path string?
+---@return string?
function M.tilde_to_HOME(path)
return path and path:gsub("^~", M.HOME()) or nil
end
+---@param path string?
+---@return string?
function M.HOME_to_tilde(path)
- return path and path:gsub("^" .. utils.lua_regex_escape(M.HOME()), "~") or nil
+ if not path then return end
+ if utils.__IS_WINDOWS then
+ local home = M.HOME()
+ if path:sub(1, #home):lower() == home:lower() then
+ path = "~" .. path:sub(#home + 1)
+ end
+ else
+ path = path:gsub("^" .. utils.lua_regex_escape(M.HOME()), "~")
+ end
+ return path
end
-function M.shorten(path, max_len)
- local sep = M.SEPARATOR
+local function find_next_separator(str, start_idx)
+ local SEPARATOR_BYTES = utils._if_win(
+ { M.fslash_byte, M.bslash_byte }, { M.fslash_byte })
+ for i = start_idx or 1, #str do
+ for _, byte in ipairs(SEPARATOR_BYTES) do
+ if string_byte(str, i) == byte then
+ return i
+ end
+ end
+ end
+end
+
+function M.shorten(path, max_len, sep)
+ -- caller can specify what separator to use
+ sep = sep or M.separator(path)
local parts = {}
local start_idx = 1
max_len = max_len and tonumber(max_len) > 0 and max_len or 1
+ if utils.__IS_WINDOWS and M.is_absolute(path) then
+ -- do not shorten "C:\" to "C", for glob to succeed
+ -- we need the paths to start with a valid drive spec
+ table.insert(parts, path:sub(1, 2))
+ start_idx = 4
+ end
repeat
- local i = find_next(path, sep, start_idx)
+ local i = find_next_separator(path, start_idx)
local end_idx = i and start_idx + math.min(i - start_idx, max_len) - 1 or nil
local part = string_sub(path, start_idx, end_idx)
if end_idx and part == "." and i - start_idx > 1 then
@@ -171,11 +301,24 @@ end
function M.lengthen(path)
-- we use 'glob_escape' to escape \{} (#548)
- path = utils.glob_escape(path)
- return vim.fn.glob(path:gsub(M.SEPARATOR, "%*" .. M.SEPARATOR)
- -- remove the starting '*/' if any
- :gsub("^%*" .. M.SEPARATOR, M.SEPARATOR)):match("[^\n]+")
- or string.format("", path)
+ local separator = M.separator(path)
+ local glob_expr = utils.glob_escape(path)
+ local glob_expr_prefix = ""
+ if M.is_absolute(path) then
+ -- don't prefix with * the leading / on UNIX or C:\ on windows
+ if utils.__IS_WINDOWS then
+ glob_expr_prefix = glob_expr:sub(1, 3)
+ glob_expr = glob_expr:sub(4)
+ else
+ glob_expr_prefix = glob_expr:sub(1, 1)
+ glob_expr = glob_expr:sub(2)
+ end
+ end
+ -- replace separator with wildcard + separator
+ glob_expr = glob_expr_prefix .. glob_expr:gsub(separator, "%*" .. separator)
+ return vim.fn.glob(glob_expr):match("[^\n]+")
+ -- or string.format("", path)
+ or string.format("", glob_expr)
end
local function lastIndexOf(haystack, needle)
@@ -189,11 +332,11 @@ local function stripBeforeLastOccurrenceOf(str, sep)
end
function M.entry_to_ctag(entry, noesc)
- local ctag = entry:match("%:.-/^?\t?(.*)/")
+ local ctag = entry:match("%:.-[/\\]^?\t?(.*)[/\\]")
-- if tag name contains a slash we could
-- have the wrong match, most tags start
-- with ^ so try to match based on that
- ctag = ctag and ctag:match("/^(.*)") or ctag
+ ctag = ctag and ctag:match("[/\\]^(.*)") or ctag
if ctag and not noesc then
-- required escapes for vim.fn.search()
-- \ ] ~ *
@@ -238,8 +381,7 @@ function M.entry_to_file(entry, opts, force_uri)
stripped = M.tilde_to_HOME(stripped)
local isURI = stripped:match("^%a+://")
-- Prepend cwd before constructing the URI (#341)
- if cwd and #cwd > 0 and not isURI and
- not M.starts_with_separator(stripped) then
+ if cwd and #cwd > 0 and not isURI and not M.is_absolute(stripped) then
stripped = M.join({ cwd, stripped })
end
-- #336: force LSP jumps using 'vim.lsp.util.jump_to_location'
@@ -264,6 +406,11 @@ function M.entry_to_file(entry, opts, force_uri)
end
local s = utils.strsplit(stripped, ":")
if not s[1] then return {} end
+ if utils.__IS_WINDOWS and M.is_absolute(stripped) then
+ -- adjust split for "C:\..."
+ s[1] = s[1] .. ":" .. s[2]
+ table.remove(s, 2)
+ end
local file = s[1]
local line = tonumber(s[2])
local col = tonumber(s[3])
@@ -328,7 +475,7 @@ function M.git_cwd(cmd, opts)
for _, a in ipairs(git_args) do
if o[a[1]] then
o[a[1]] = a.noexpand and o[a[1]] or vim.fn.expand(o[a[1]])
- args = args .. ("%s %s "):format(a[2], vim.fn.shellescape(o[a[1]]))
+ args = args .. ("%s %s "):format(a[2], libuv.shellescape(o[a[1]]))
end
end
cmd = cmd:gsub("^git ", "git " .. args)
diff --git a/lua/fzf-lua/previewer/builtin.lua b/lua/fzf-lua/previewer/builtin.lua
index 6c64623c..2ef5e659 100644
--- a/lua/fzf-lua/previewer/builtin.lua
+++ b/lua/fzf-lua/previewer/builtin.lua
@@ -31,10 +31,10 @@ function Previewer.base:new(o, opts, fzf_win)
self.title_pos = self.win.winopts.preview.title_pos
self.winopts = self.win.winopts.preview.winopts
self.syntax = default(o.syntax, true)
- self.syntax_delay = default(o.syntax_delay, 0)
- self.syntax_limit_b = default(o.syntax_limit_b, 1024 * 1024)
- self.syntax_limit_l = default(o.syntax_limit_l, 0)
- self.limit_b = default(o.limit_b, 1024 * 1024 * 10)
+ self.syntax_delay = tonumber(default(o.syntax_delay, 0))
+ self.syntax_limit_b = tonumber(default(o.syntax_limit_b, 1024 * 1024))
+ self.syntax_limit_l = tonumber(default(o.syntax_limit_l, 0))
+ self.limit_b = tonumber(default(o.limit_b, 1024 * 1024 * 10))
self.treesitter = o.treesitter or {}
self.toggle_behavior = o.toggle_behavior
self.ext_ft_override = o.ext_ft_override
@@ -108,12 +108,12 @@ end
function Previewer.base:preview_is_terminal()
if not self.win or not self.win:validate_preview() then return end
- return vim.fn.getwininfo(self.win.preview_winid)[1].terminal == 1
+ return utils.getwininfo(self.win.preview_winid).terminal == 1
end
function Previewer.base:get_tmp_buffer()
local tmp_buf = api.nvim_create_buf(false, true)
- api.nvim_buf_set_option(tmp_buf, "bufhidden", "wipe")
+ vim.bo[tmp_buf].bufhidden = "wipe"
return tmp_buf
end
@@ -184,7 +184,7 @@ function Previewer.base:cache_buffer(bufnr, key, min_winopts)
self.cached_bufnrs[tostring(bufnr)] = true
self.cached_buffers[key] = { bufnr = bufnr, min_winopts = min_winopts }
-- remove buffer auto-delete since it's now cached
- api.nvim_buf_set_option(bufnr, "bufhidden", "hide")
+ vim.bo[bufnr].bufhidden = "hide"
end
function Previewer.base:clear_cached_buffers()
@@ -303,7 +303,7 @@ function Previewer.base:zero(_)
-- currently awaiting an upstream fix:
-- https://github.com/junegunn/fzf/issues/3516
--
- self._zero_lock = self._zero_lock or vim.fn.tempname()
+ self._zero_lock = self._zero_lock or path.normalize(vim.fn.tempname())
local act = string.format("execute-silent(mkdir %s && %s)",
libuv.shellescape(self._zero_lock),
shell.raw_action(function(_, _, _)
@@ -368,16 +368,16 @@ function Previewer.base:scroll(direction)
-- user scrolls, the highlight is no longer relevant (#462).
-- Conditionally toggle 'cursorline' based on cursor position
if self.orig_pos and self.winopts.cursorline then
- local wininfo = vim.fn.getwininfo(preview_winid)
- if wininfo and wininfo[1] and
- self.orig_pos[1] >= wininfo[1].topline and
- self.orig_pos[1] <= wininfo[1].botline then
+ local wininfo = utils.getwininfo(preview_winid)
+ if wininfo and
+ self.orig_pos[1] >= wininfo.topline and
+ self.orig_pos[1] <= wininfo.botline then
-- reset cursor pos even when it's already there, no bigggie
-- local curpos = vim.api.nvim_win_get_cursor(preview_winid)
vim.api.nvim_win_set_cursor(preview_winid, self.orig_pos)
- vim.api.nvim_win_set_option(preview_winid, "cursorline", true)
+ vim.wo[preview_winid].cursorline = true
else
- vim.api.nvim_win_set_option(preview_winid, "cursorline", false)
+ vim.wo[preview_winid].cursorline = false
end
end
self.win:update_scrollbar()
@@ -408,10 +408,10 @@ function Previewer.buffer_or_file:should_clear_preview(_)
end
function Previewer.buffer_or_file:should_load_buffer(entry)
- -- we don't have a previous entry to compare to
- -- return 'true' so the buffer will be loaded in
- -- ::populate_preview_buf
- if not self.loaded_entry then return true end
+ -- we don't have a previous entry to compare to or `do_not_cache` is set meaning
+ -- it's a terminal command (chafa, viu, ueberzug) which requires a reload
+ -- return 'true' so the buffer will be loaded in ::populate_preview_buf
+ if not self.loaded_entry or self.loaded_entry.do_not_cache then return true end
if type(entry) == "string" then
entry = self:parse_entry(entry)
end
@@ -429,7 +429,7 @@ function Previewer.buffer_or_file:start_ueberzug()
utils.io_system({ "mkfifo", self._ueberzug_fifo })
self._ueberzug_job = vim.fn.jobstart({ "sh", "-c",
("tail --follow %s | ueberzug layer --parser json")
- :format(vim.fn.shellescape(self._ueberzug_fifo))
+ :format(libuv.shellescape(self._ueberzug_fifo))
}, {
on_exit = function(_, rc, _)
if rc ~= 0 and rc ~= 143 then
@@ -505,7 +505,7 @@ function Previewer.buffer_or_file:populate_terminal_cmd(tmpbuf, cmd, entry)
width = wincfg.width,
height = wincfg.height,
scaler = self.ueberzug_scaler,
- path = path.starts_with_separator(entry.path) and entry.path or
+ path = path.is_absolute(entry.path) and entry.path or
path.join({ self.opts.cwd or uv.cwd(), entry.path }),
}
local json = vim.json.encode(params)
@@ -576,6 +576,8 @@ end
function Previewer.buffer_or_file:populate_preview_buf(entry_str)
if not self.win or not self.win:validate_preview() then return end
+ -- stop ueberzug shell job
+ self:stop_ueberzug()
local entry = self:parse_entry(entry_str)
if vim.tbl_isempty(entry) then return end
if entry.bufnr and not api.nvim_buf_is_loaded(entry.bufnr)
@@ -583,7 +585,7 @@ function Previewer.buffer_or_file:populate_preview_buf(entry_str)
-- buffer is not loaded, can happen when calling "lines" with `set nohidden`
-- or when starting nvim with an arglist, fix entry.path since it contains
-- filename only
- entry.path = path.relative(vim.api.nvim_buf_get_name(entry.bufnr), vim.loop.cwd())
+ entry.path = path.relative_to(vim.api.nvim_buf_get_name(entry.bufnr), vim.loop.cwd())
end
if not self:should_load_buffer(entry) then
-- same file/buffer as previous entry
@@ -595,9 +597,7 @@ function Previewer.buffer_or_file:populate_preview_buf(entry_str)
-- already populated
return
end
- -- stop ueberzug shell job
self.clear_on_redraw = false
- self:stop_ueberzug()
-- kill previously running terminal jobs
-- when using external commands extension map
if self._job_id and self._job_id > 0 then
@@ -608,7 +608,7 @@ function Previewer.buffer_or_file:populate_preview_buf(entry_str)
-- WE NO LONGER REUSE THE CURRENT BUFFER
-- this changes the buffer's 'getbufinfo[1].lastused'
-- which messes up our `buffers()` sort
- entry.filetype = vim.api.nvim_buf_get_option(entry.bufnr, "filetype")
+ entry.filetype = vim.bo[entry.bufnr].filetype
local lines = vim.api.nvim_buf_get_lines(entry.bufnr, 0, -1, false)
local tmpbuf = self:get_tmp_buffer()
vim.api.nvim_buf_set_lines(tmpbuf, 0, -1, false, lines)
@@ -719,7 +719,7 @@ local ts_attach_08 = function(bufnr, ft)
config.additional_vim_regex_highlighting
and (not is_table or vim.tbl_contains(config.additional_vim_regex_highlighting, lang))
then
- vim.api.nvim_buf_set_option(bufnr, "syntax", ft)
+ vim.bo[bufnr].syntax = ft
end
return true
end
@@ -802,7 +802,7 @@ function Previewer.buffer_or_file:do_syntax(entry)
end
end
if not ts_enabled or not ts_success then
- pcall(vim.api.nvim_buf_set_option, bufnr, "syntax", ft)
+ pcall(function() vim.bo[bufnr].syntax = ft end)
end
end)()
end
@@ -812,8 +812,7 @@ function Previewer.buffer_or_file:do_syntax(entry)
-- filetype was saved from a loaded buffer
-- this helps avoid losing highlights for help buffers
-- which are '.txt' files with 'ft=help'
- -- api.nvim_buf_set_option(bufnr, 'filetype', entry.filetype)
- pcall(api.nvim_buf_set_option, bufnr, "filetype", entry.filetype)
+ pcall(function() vim.bo[bufnr].filetype = "help" end)
else
-- prepend the buffer number to the path and
-- set as buffer name, this makes sure 'filetype detect'
@@ -869,14 +868,15 @@ function Previewer.buffer_or_file:update_border(entry)
local filepath = entry.path
if filepath then
if self.opts.cwd then
- filepath = path.relative(entry.path, self.opts.cwd)
+ filepath = path.relative_to(entry.path, self.opts.cwd)
end
filepath = path.HOME_to_tilde(filepath)
end
local title = filepath or entry.uri
-- was transform function defined?
if self.title_fnamemodify then
- title = self.title_fnamemodify(title)
+ local wincfg = vim.api.nvim_win_get_config(self.win.border_winid)
+ title = self.title_fnamemodify(title, wincfg and wincfg.width)
end
if entry.bufnr then
title = string.format("buf %d: %s", entry.bufnr, title)
@@ -896,8 +896,11 @@ function Previewer.buffer_or_file:preview_buf_post(entry, min_winopts)
-- syntax highlighting
if self.syntax then
if self.syntax_delay > 0 then
+ local syntax_bufnr = self.preview_bufnr
vim.defer_fn(function()
- self:do_syntax(entry)
+ if self.preview_bufnr == syntax_bufnr then
+ self:do_syntax(entry)
+ end
end, self.syntax_delay)
else
self:do_syntax(entry)
@@ -928,7 +931,7 @@ end
function Previewer.help_tags:parse_entry(entry_str)
local tag = entry_str:match("^[^%s]+")
- local vimdoc = entry_str:match("[^%s]+$")
+ local vimdoc = entry_str:match(string.format("[^%s]+$", utils.nbsp))
return {
htag = tag,
hregex = ([[\V*%s*]]):format(tag:gsub([[\]], [[\\]])),
@@ -983,8 +986,7 @@ function Previewer.man_pages:new(o, opts, fzf_win)
end
function Previewer.man_pages:parse_entry(entry_str)
- return entry_str:match("[^[,( ]+")
- -- return require'fzf-lua.providers.manpages'.getmanpage(entry_str)
+ return require("fzf-lua.providers.manpages").manpage_sh_arg(entry_str)
end
function Previewer.man_pages:populate_preview_buf(entry_str)
@@ -995,7 +997,7 @@ function Previewer.man_pages:populate_preview_buf(entry_str)
local tmpbuf = self:get_tmp_buffer()
-- vim.api.nvim_buf_set_option(tmpbuf, 'modifiable', true)
vim.api.nvim_buf_set_lines(tmpbuf, 0, -1, false, output)
- vim.api.nvim_buf_set_option(tmpbuf, "filetype", self.filetype)
+ vim.bo[tmpbuf].filetype = self.filetype
self:set_preview_buf(tmpbuf)
self.win:update_scrollbar()
end
@@ -1024,7 +1026,7 @@ function Previewer.marks:parse_entry(entry_str)
else
filepath = res
end
- filepath = path.relative(filepath, vim.loop.cwd())
+ filepath = path.relative_to(filepath, vim.loop.cwd())
end
return {
bufnr = bufnr,
@@ -1047,7 +1049,7 @@ function Previewer.jumps:parse_entry(entry_str)
if filepath then
local ok, res = pcall(vim.fn.expand, filepath)
if ok then
- filepath = path.relative(res, vim.loop.cwd())
+ filepath = path.relative_to(res, vim.loop.cwd())
end
if not vim.loop.fs_stat(filepath) then
-- file is not accessible,
@@ -1215,13 +1217,13 @@ function Previewer.quickfix:populate_preview_buf(entry_str)
local lines = {}
for _, e in ipairs(qf_list.items) do
table.insert(lines, string.format("%s|%d col %d|%s",
- path.HOME_to_tilde(path.relative(
+ path.HOME_to_tilde(path.relative_to(
vim.api.nvim_buf_get_name(e.bufnr), vim.loop.cwd())),
e.lnum, e.col, e.text))
end
self.tmpbuf = self:get_tmp_buffer()
vim.api.nvim_buf_set_lines(self.tmpbuf, 0, -1, false, lines)
- vim.api.nvim_buf_set_option(self.tmpbuf, "filetype", "qf")
+ vim.bo[self.tmpbuf].filetype = "qf"
self:set_preview_buf(self.tmpbuf)
self.win:update_title(string.format("%s: %s", nr, qf_list.title))
self.win:update_scrollbar()
@@ -1254,12 +1256,12 @@ function Previewer.autocmds:populate_preview_buf(entry_str)
self._is_vimL_command = false
if entry.path == "" then
self._is_vimL_command = true
- entry.path = entry_str:match("[^:]+ā")
+ entry.path = entry_str:match("[^:|]+ā")
local viml = entry_str:match("[^ā]+$")
local lines = vim.split(viml, "\n")
local tmpbuf = self:get_tmp_buffer()
vim.api.nvim_buf_set_lines(tmpbuf, 0, -1, false, lines)
- vim.api.nvim_buf_set_option(tmpbuf, "filetype", "vim")
+ vim.bo[tmpbuf].filetype = "vim"
self:set_preview_buf(tmpbuf)
self:preview_buf_post(entry)
else
@@ -1287,7 +1289,7 @@ function Previewer.keymaps:populate_preview_buf(entry_str)
local lines = utils.strsplit(entry.vmap:match("[^%s]+$"), "\n")
local tmpbuf = self:get_tmp_buffer()
vim.api.nvim_buf_set_lines(tmpbuf, 0, -1, false, lines)
- vim.api.nvim_buf_set_option(tmpbuf, "filetype", "vim")
+ vim.bo[tmpbuf].filetype = "vim"
self:set_preview_buf(tmpbuf)
local title_fnamemodify = self.title_fnamemodify
self.title_fnamemodify = nil
diff --git a/lua/fzf-lua/previewer/codeaction.lua b/lua/fzf-lua/previewer/codeaction.lua
index defa8b70..5735ec01 100644
--- a/lua/fzf-lua/previewer/codeaction.lua
+++ b/lua/fzf-lua/previewer/codeaction.lua
@@ -13,7 +13,7 @@ local function get_lines(bufnr)
end
local function get_eol(bufnr)
- local ff = vim.api.nvim_buf_get_option(bufnr, "fileformat")
+ local ff = vim.bo[bufnr].fileformat
if ff == "dos" then
return "\r\n"
elseif ff == "unix" then
@@ -68,7 +68,10 @@ local function diff_workspace_edit(workspace_edit, offset_encoding, diff_opts)
local path = vim.fn.fnamemodify(vim.uri_to_fname(change.uri), ":.")
table.insert(diff, string.format("diff --code-actions a/%s b/%s", path, path))
- table.insert(diff, "new file")
+ -- delta needs file mode
+ table.insert(diff, "new file mode 100644")
+ -- diff-so-fancy needs index
+ table.insert(diff, "index 0000000..fffffff")
table.insert(diff, "")
elseif change.kind == "delete" then
local path = vim.fn.fnamemodify(vim.uri_to_fname(change.uri), ":.")
@@ -139,55 +142,93 @@ local function diff_tuple(err, tuple, diff_opts)
end
-- https://github.com/neovim/neovim/blob/v0.9.4/runtime/lua/vim/lsp/buf.lua#L666
-local function preview_action_tuple(tuple, diff_opts, callback)
+local function preview_action_tuple(self, idx, callback)
+ local tuple = self.opts._items[idx]
-- neovim changed the ui.select params with 0.10.0 (#947)
-- { client_id, action } ==> { ctx = , action = }
if tuple.ctx then
tuple = { tuple.ctx.client_id, tuple.action }
end
- local client = assert(vim.lsp.get_client_by_id(tuple[1]))
+ -- First check our resolved action cache, if "codeAction/resolve" failed, ignore
+ -- the error (we already alerted the user about it in `handle_resolved_response`)
+ -- and display the default "unsupported" message from the original action
+ if self._resolved_actions[idx] then
+ local resolved = self._resolved_actions[idx]
+ return diff_tuple(nil, not resolved.err and resolved.tuple or tuple, self.diff_opts)
+ end
+ -- Not found in cache, check if the client supports code action resolving
+ local client_id = tuple[1]
+ local client = assert(vim.lsp.get_client_by_id(client_id))
local action = tuple[2]
- if
- not action.edit
- and client
- and vim.tbl_get(client.server_capabilities, "codeActionProvider", "resolveProvider")
- then
- local function on_result(diff_callback, err, resolved_action)
+ local supports_resolve = utils.__HAS_NVIM_010
+ -- runtime/lua/lsp/buf.lua:on_user_choice
+ and (function()
+ ---@var choice {action: lsp.Command|lsp.CodeAction, ctx: lsp.HandlerContext}
+ local ms = require("vim.lsp.protocol").Methods
+ local choice = self.opts._items[idx]
+ local bufnr = assert(choice.ctx.bufnr, "Must have buffer number")
+ local reg = client.dynamic_capabilities:get(ms.textDocument_codeAction, { bufnr = bufnr })
+ return vim.tbl_get(reg or {}, "registerOptions", "resolveProvider")
+ or client.supports_method(ms.codeAction_resolve)
+ end)()
+ -- prior to nvim 0.10 we could check `client.server_capabilities`
+ or vim.tbl_get(client.server_capabilities, "codeActionProvider", "resolveProvider")
+ if not action.edit and client and supports_resolve then
+ -- Action is not a worksapce edit, attempt to resolve the code action
+ -- in case it resolves to a workspace edit
+ local function handle_resolved_response(err, resolved_action)
if err then
- return diff_callback(err, tuple, diff_opts)
- else
- return diff_callback(err, { tuple[1], resolved_action }, diff_opts)
+ -- alert the user "codeAction/resolve" request failed
+ utils.warn(diff_tuple(err, nil, self.diff_opts)[1])
end
+ local resolved = {
+ err = err,
+ -- Due to a bug in `typescript-tools.nvim` only the first call to `codeAction/resolve`
+ -- returns a valid action (non-nil), return nil tuple if the action is nil (#949)
+ tuple = resolved_action and { client_id, resolved_action } or nil
+ }
+ self._resolved_actions[idx] = resolved
+ -- HACK: due to upstream bug with jdtls calling resolve messes
+ -- errs the workspace edit with "-32603: Internal error." (#1007)
+ if not err and client.name == "jdtls" then
+ if utils.__HAS_NVIM_010 then
+ self.opts._items[idx].action = resolved_action
+ else
+ self.opts._items[idx][2] = resolved_action
+ end
+ end
+ return resolved.tuple
end
-
if callback then
client.request("codeAction/resolve", action, function(err, resolved_action)
- on_result(callback, err, resolved_action)
+ local resolved_tuple = handle_resolved_response(err, resolved_action)
+ callback(nil, not err and resolved_tuple or tuple)
end)
return { string.format("Resolving action (%s)...", action.kind) }
else
local res = client.request_sync("codeAction/resolve", action)
local err, resolved_action = res and res.err, res and res.result
- if type(err) == "table" or type(resolved_action) == "table" then
- return on_result(diff_tuple, err, resolved_action)
- else
- -- display the default "unsupported" message
- return diff_tuple(nil, tuple, diff_opts)
- end
+ local resolved_tuple = handle_resolved_response(err, resolved_action)
+ return diff_tuple(nil, not err and resolved_tuple or tuple, self.diff_opts)
end
else
- return diff_tuple(nil, tuple, diff_opts)
+ return diff_tuple(nil, tuple, self.diff_opts)
end
end
M.builtin = builtin.base:extend()
+M.builtin.preview_action_tuple = preview_action_tuple
function M.builtin:new(o, opts, fzf_win)
assert(opts._ui_select and opts._ui_select.kind == "codeaction")
M.builtin.super.new(self, o, opts, fzf_win)
setmetatable(self, M.builtin)
self.diff_opts = o.diff_opts
+ self._resolved_actions = {}
+ for i, _ in ipairs(self.opts._items) do
+ self._resolved_actions[i] = false
+ end
return self
end
@@ -204,8 +245,7 @@ function M.builtin:populate_preview_buf(entry_str)
if not self.win or not self.win:validate_preview() then return end
local idx = tonumber(entry_str:match("^%d+%."))
assert(type(idx) == "number")
- local tuple = self.opts._items[idx]
- local lines = preview_action_tuple(tuple, self.diff_opts,
+ local lines = self:preview_action_tuple(idx,
-- use the async version for "codeAction/resolve"
function(err, resolved_tuple)
if vim.api.nvim_buf_is_valid(self.tmpbuf) then
@@ -215,13 +255,14 @@ function M.builtin:populate_preview_buf(entry_str)
end)
self.tmpbuf = self:get_tmp_buffer()
vim.api.nvim_buf_set_lines(self.tmpbuf, 0, -1, false, lines)
- vim.api.nvim_buf_set_option(self.tmpbuf, "filetype", "git")
+ vim.bo[self.tmpbuf].filetype = "git"
self:set_preview_buf(self.tmpbuf)
self.win:update_title(string.format(" Action #%d ", idx))
self.win:update_scrollbar()
end
M.native = native.base:extend()
+M.native.preview_action_tuple = preview_action_tuple
function M.native:new(o, opts, fzf_win)
assert(opts._ui_select and opts._ui_select.kind == "codeaction")
@@ -232,6 +273,10 @@ function M.native:new(o, opts, fzf_win)
self.pager = self.pager()
end
self.diff_opts = o.diff_opts
+ self._resolved_actions = {}
+ for i, _ in ipairs(self.opts._items) do
+ self._resolved_actions[i] = false
+ end
return self
end
@@ -240,12 +285,11 @@ function M.native:cmdline(o)
local act = shell.raw_action(function(entries, _, _)
local idx = tonumber(entries[1]:match("^%d+%."))
assert(type(idx) == "number")
- local tuple = self.opts._items[idx]
- local lines = preview_action_tuple(tuple, self.diff_opts)
+ local lines = self:preview_action_tuple(idx)
return table.concat(lines, "\r\n")
end, "{}", self.opts.debug)
if self.pager and #self.pager > 0 and vim.fn.executable(self.pager:match("[^%s]+")) == 1 then
- act = act .. " | " .. self.pager
+ act = act .. " | " .. utils._if_win_normalize_vars(self.pager)
end
return act
end
diff --git a/lua/fzf-lua/previewer/fzf.lua b/lua/fzf-lua/previewer/fzf.lua
index 4c72d331..b4ee701d 100644
--- a/lua/fzf-lua/previewer/fzf.lua
+++ b/lua/fzf-lua/previewer/fzf.lua
@@ -1,6 +1,7 @@
local path = require "fzf-lua.path"
local shell = require "fzf-lua.shell"
local utils = require "fzf-lua.utils"
+local libuv = require "fzf-lua.libuv"
local Object = require "fzf-lua.class"
local Previewer = {}
@@ -16,6 +17,7 @@ function Previewer.base:new(o, opts)
self.cmd = self.cmd()
end
self.args = o.args or "";
+ self.preview_offset = o.preview_offset
self.opts = opts;
return self
end
@@ -24,7 +26,10 @@ function Previewer.base:preview_window(_)
return nil
end
-function Previewer.base:preview_offset()
+function Previewer.base:_preview_offset()
+ if self.opts.preview_offset or self.preview_offset then
+ return self.opts.preview_offset or self.preview_offset
+ end
--[[
#
# Explanation of the fzf preview offset options:
@@ -42,7 +47,6 @@ function Previewer.base:preview_offset()
end
function Previewer.base:fzf_delimiter()
- if not self.opts.line_field_index then return end
-- set delimiter to ':'
-- entry format is 'file:line:col: text'
local delim = self.opts.fzf_opts and self.opts.fzf_opts["--delimiter"]
@@ -50,7 +54,7 @@ function Previewer.base:fzf_delimiter()
delim = "[:]"
elseif not delim:match(":") then
if delim:match("%[.*%]") then
- delim = delim:match("(%[.*)%]") .. ":]"
+ delim = delim:gsub("%]", ":]")
else
-- remove surrounding quotes
delim = delim:match("^'?(.*)'$?") or delim
@@ -173,7 +177,8 @@ end
function Previewer.cmd_async:parse_entry_and_verify(entrystr)
local entry = path.entry_to_file(entrystr, self.opts)
- local filepath = entry.bufname or entry.path or ""
+ -- make relative for bat's header display
+ local filepath = path.relative_to(entry.bufname or entry.path or "", vim.loop.cwd())
if self.opts._ctag then
entry.ctag = path.entry_to_ctag(entry.stripped, true)
if entry.line <= 1 then
@@ -189,8 +194,9 @@ function Previewer.cmd_async:parse_entry_and_verify(entrystr)
local errcmd = nil
-- verify the file exists on disk and is accessible
if #filepath == 0 or not vim.loop.fs_stat(filepath) then
- errcmd = ([[echo "%s: NO SUCH FILE OR ACCESS DENIED"]]):format(
- filepath and #filepath > 0 and vim.fn.shellescape(filepath) or "")
+ errcmd = "echo " .. libuv.shellescape(
+ string.format("'%s: NO SUCH FILE OR ACCESS DENIED",
+ filepath and #filepath > 0 and filepath or ""))
end
return filepath, entry, errcmd
end
@@ -200,9 +206,7 @@ function Previewer.cmd_async:cmdline(o)
local act = shell.raw_preview_action_cmd(function(items)
local filepath, _, errcmd = self:parse_entry_and_verify(items[1])
local cmd = errcmd or ("%s %s %s"):format(
- self.cmd, self.args, vim.fn.shellescape(filepath))
- -- uncomment to see the command in the preview window
- -- cmd = vim.fn.shellescape(cmd)
+ self.cmd, self.args, libuv.shellescape(filepath))
return cmd
end, "{}", self.opts.debug)
return act
@@ -210,6 +214,37 @@ end
Previewer.bat_async = Previewer.cmd_async:extend()
+function Previewer.bat_async:_preview_offset()
+ if self.opts.preview_offset or self.preview_offset then
+ return self.opts.preview_offset or self.preview_offset
+ end
+ --[[
+ #
+ # Explanation of the fzf preview offset options:
+ #
+ # ~3 Top 3 lines as the fixed header
+ # +{2} Base scroll offset extracted from the second field
+ # +3 Extra offset to compensate for the 3-line header
+ # /2 Put in the middle of the preview area
+ #
+ '--preview-window '~3:+{2}+3/2''
+ ]]
+ if not self.args or not self.args:match("%-%-style=default") then
+ -- we don't need affixed header unless we use bat default style
+ -- TODO: shuld also adjust for "--style=header-filename"
+ if self.opts.line_field_index then
+ return ("+%s-/2"):format(self.opts.line_field_index)
+ end
+ else
+ if self.opts.line_field_index then
+ return ("~3:+%s+3/2"):format(self.opts.line_field_index)
+ else
+ -- no line offset, affix header
+ return "~3"
+ end
+ end
+end
+
function Previewer.bat_async:new(o, opts)
Previewer.bat_async.super.new(self, o, opts)
self.theme = o.theme
@@ -234,9 +269,7 @@ function Previewer.bat_async:cmdline(o)
self.theme and string.format([[--theme="%s"]], self.theme) or "",
self.opts.line_field_index and string.format("--highlight-line=%d", entry.line) or "",
line_range,
- vim.fn.shellescape(filepath))
- -- uncomment to see the command in the preview window
- -- cmd = vim.fn.shellescape(cmd)
+ libuv.shellescape(filepath))
return cmd
end, "{}", self.opts.debug)
return act
@@ -298,7 +331,7 @@ function Previewer.git_diff:cmdline(o)
elseif is_untracked then
local stat = vim.loop.fs_stat(file.path)
if stat and stat.type == "directory" then
- cmd = "ls -la"
+ cmd = utils._if_win({ "dir" }, { "ls", "-la" })
else
cmd = self.cmd_untracked
end
@@ -307,7 +340,9 @@ function Previewer.git_diff:cmdline(o)
local pager = ""
if self.pager and #self.pager > 0 and
vim.fn.executable(self.pager:match("[^%s]+")) == 1 then
- pager = "| " .. self.pager
+ -- style 2: as we are unable to use %var% within a "cmd /c" without !var! expansion
+ -- https://superuser.com/questions/223104/setting-and-using-variable-within-same-command-line-in-windows-cmd-ex
+ pager = "| " .. utils._if_win_normalize_vars(self.pager, 2)
end
-- with default commands we add the filepath at the end.
-- If the user configured a more complex command, e.g.:
@@ -316,7 +351,7 @@ function Previewer.git_diff:cmdline(o)
-- }
-- we use ':format' directly on the user's command, see
-- issue #392 for more info (limiting diff output width)
- local fname_escaped = vim.fn.shellescape(file.path)
+ local fname_escaped = libuv.shellescape(file.path)
if cmd:match("[<{]file[}>]") then
cmd = cmd:gsub("[<{]file[}>]", fname_escaped)
elseif cmd:match("%%s") then
@@ -324,12 +359,17 @@ function Previewer.git_diff:cmdline(o)
else
cmd = string.format("%s %s", cmd, fname_escaped)
end
- cmd = ("LINES=%d;COLUMNS=%d;FZF_PREVIEW_LINES=%d;FZF_PREVIEW_COLUMNS=%d;%s %s")
- :format(fzf_lines, fzf_columns, fzf_lines, fzf_columns, cmd, pager)
- cmd = "sh -c " .. vim.fn.shellescape(cmd)
- -- uncomment to see the command in the preview window
- -- cmd = vim.fn.shellescape(cmd)
- return cmd
+ local env = {
+ ["LINES"] = fzf_lines,
+ ["COLUMNS"] = fzf_columns,
+ ["FZF_PREVIEW_LINES"] = fzf_lines,
+ ["FZF_PREVIEW_COLUMNS"] = fzf_columns,
+ }
+ local setenv = utils.shell_setenv_str(env)
+ cmd = string.format("%s %s %s", table.concat(setenv, " "), cmd, pager)
+ -- TODO: exlpore why passing env (which we btw don't need anymore)
+ -- makes git-delta use a different syntax theme
+ return { cmd = cmd, env = nil }
end, "{}", self.opts.debug)
return act
end
@@ -357,11 +397,8 @@ end
function Previewer.man_pages:cmdline(o)
o = o or {}
local act = shell.raw_preview_action_cmd(function(items)
- -- local manpage = require'fzf-lua.providers.manpages'.getmanpage(items[1])
- local manpage = items[1]:match("[^[,( ]+")
- local cmd = self.cmd:format(vim.fn.shellescape(manpage))
- -- uncomment to see the command in the preview window
- -- cmd = vim.fn.shellescape(cmd)
+ local manpage = require("fzf-lua.providers.manpages").manpage_sh_arg(items[1])
+ local cmd = self.cmd:format(manpage)
return cmd
end, "{}", self.opts.debug)
return act
@@ -369,6 +406,10 @@ end
Previewer.help_tags = Previewer.base:extend()
+function Previewer.help_tags:fzf_delimiter()
+ return self.opts.fzf_opts and self.opts.fzf_opts["--delimiter"] or nil
+end
+
function Previewer.help_tags:new(o, opts)
Previewer.help_tags.super.new(self, o, opts)
self.cmd = self.cmd or vim.fn.executable("bat") == 1
@@ -380,10 +421,10 @@ end
function Previewer.help_tags:cmdline(o)
o = o or {}
local act = shell.raw_preview_action_cmd(function(items)
- local vimdoc = items[1]:match("[^%s]+$")
+ local vimdoc = items[1]:match(string.format("[^%s]+$", utils.nbsp))
local tag = items[1]:match("^[^%s]+")
local ext = path.extension(vimdoc)
- local cmd = self.cmd:format(vim.fn.shellescape(vimdoc))
+ local cmd = self.cmd:format(libuv.shellescape(vimdoc))
-- If 'bat' is available attempt to get the helptag line
-- and start the display of the help file from the tag
if self.cmd:match("^bat ") then
@@ -395,8 +436,6 @@ function Previewer.help_tags:cmdline(o)
cmd = cmd .. string.format(" --line-range=%d:", tonumber(line))
end
end
- -- uncomment to see the command in the preview window
- -- cmd = vim.fn.shellescape(cmd)
return cmd
end, "{}", self.opts.debug)
return act
diff --git a/lua/fzf-lua/profiles/README.md b/lua/fzf-lua/profiles/README.md
index 7d7478e0..11e04a78 100644
--- a/lua/fzf-lua/profiles/README.md
+++ b/lua/fzf-lua/profiles/README.md
@@ -19,6 +19,7 @@ telescope defaults with `bat` previewer:
| Profile | Details |
| ---------------- | ------------------------------------------ |
| `default` | fzf-lua defaults, uses neovim "builtin" previewer and devicons (if available) for git/files/buffers |
+| `default-title` | fzf-lua defaults, using title instead of prompt |
| `fzf-native` | utilizes fzf's native previewing ability in the terminal where possible using `bat` for previews |
| `fzf-tmux` | similar to `fzf-native` and opens in a tmux popup (requires tmux > 3.2) |
| `fzf-vim` | closest to `fzf.vim`'s defaults (+icons), also sets up user commands (`:Files`, `:Rg`, etc) |
diff --git a/lua/fzf-lua/profiles/borderless_full.lua b/lua/fzf-lua/profiles/borderless_full.lua
index 3730499f..efe7edc1 100644
--- a/lua/fzf-lua/profiles/borderless_full.lua
+++ b/lua/fzf-lua/profiles/borderless_full.lua
@@ -3,20 +3,10 @@ local hls = {
sel = "PmenuSel",
title = "IncSearch"
}
-local function title(str, opts)
- return vim.tbl_deep_extend("keep", opts or {},
- {
- prompt = "āÆ ",
- winopts = {
- -- title = { { " " .. str .. " ", hls.title } },
- title = " " .. str .. " ",
- title_pos = "center",
- }
- })
-end
return {
- desc = "borderless and not so minimalistic",
- winopts = {
+ { "default-title" }, -- base profile
+ desc = "borderless and not so minimalistic",
+ winopts = {
border = { " ", " ", " ", " ", " ", " ", " ", " " },
preview = {
scrollbar = "float",
@@ -24,7 +14,7 @@ return {
title_pos = "center",
},
},
- hls = {
+ hls = {
title = hls.title,
border = hls.bg,
preview_title = hls.title,
@@ -32,66 +22,11 @@ return {
scrollfloat_e = "",
scrollfloat_f = hls.sel,
},
- fzf_colors = {
+ fzf_colors = {
["gutter"] = { "bg", hls.bg },
["bg"] = { "bg", hls.bg },
["bg+"] = { "bg", hls.sel },
["fg+"] = { "fg", hls.sel },
},
- files = title("Files"),
- buffers = title("Buffers"),
- tabs = title("Tabs"),
- lines = title("Lines"),
- blines = title("Buffer Lines"),
- grep = title("Search", { rg_glob = true }),
- git = {
- files = title("Git Files"),
- status = title("Git Status"),
- commits = title("Git Commits"),
- bcommits = title("Git BCommits"),
- branches = title("Git Branches"),
- stash = title("Git Stash"),
- },
- args = title("Args"),
- oldfiles = title("Oldfiles"),
- quickfix = title("Quickfix List"),
- quickfix_stack = title("Quickfix List Stack"),
- loclist = title("Location List"),
- loclist_stack = title("Location List Stack"),
- tags = title("Tags"),
- btags = title("Buffer Tags"),
- colorschemes = title("Colorschemes"),
- highlights = title("Highlights"),
- helptags = title("Neovim Help"),
- manpages = title("Man Pages"),
- lsp = title("LSP", {
- prompt = false,
- symbols = title("LSP", { prompt = false }),
- finder = title("LSP Finder"),
- code_actions = title("Code Actions"),
- }),
- diagnostics = title("Diagnostics"),
- builtin = title("FzfLua Builtin"),
- profiles = title("FzfLua Profiles"),
- marks = title("Marks"),
- jumps = title("Jumps"),
- tagstack = title("Tagstack"),
- commands = title("Commands"),
- autocmds = title("Autocmds"),
- command_history = title("Command history"),
- search_history = title("Search history"),
- registers = title("Registers"),
- keymaps = title("Keymaps"),
- spell_suggest = title("Spell Suggestions"),
- filetypes = title("Filetypes"),
- packadd = title("Packer Packadd"),
- menus = title("Menus"),
- tmux = title("Tmux Buffers"),
- dap = {
- commands = title("DAP Commands"),
- configurations = title("DAP Configurations"),
- variables = title("DAP Variables"),
- frames = title("DAP Frames"),
- breakpoints = title("DAP Breakpoints"),
- },
+ grep = { rg_glob = true },
}
diff --git a/lua/fzf-lua/profiles/default-title.lua b/lua/fzf-lua/profiles/default-title.lua
new file mode 100644
index 00000000..55b9390d
--- /dev/null
+++ b/lua/fzf-lua/profiles/default-title.lua
@@ -0,0 +1,72 @@
+local function title(str, opts)
+ return vim.tbl_deep_extend("keep", opts or {},
+ {
+ prompt = false,
+ winopts = {
+ -- title = { { " " .. str .. " ", hls.title } },
+ title = " " .. str .. " ",
+ title_pos = "center",
+ }
+ })
+end
+return {
+ desc = "defaults using title instead of prompt",
+ files = title("Files"),
+ buffers = title("Buffers"),
+ tabs = title("Tabs"),
+ lines = title("Lines"),
+ blines = title("Buffer Lines"),
+ grep = title("Grep", { prompt = "> " }),
+ git = {
+ files = title("Git Files"),
+ status = title("Git Status"),
+ commits = title("Git Commits"),
+ bcommits = title("Git BCommits"),
+ branches = title("Git Branches"),
+ stash = title("Git Stash"),
+ },
+ args = title("Args"),
+ oldfiles = title("Oldfiles"),
+ quickfix = title("Quickfix List"),
+ quickfix_stack = title("Quickfix List Stack"),
+ loclist = title("Location List"),
+ loclist_stack = title("Location List Stack"),
+ tags = title("Tags"),
+ btags = title("Buffer Tags"),
+ colorschemes = title("Colorschemes"),
+ awesome_colorschemes = title("Awesome Colorschemes"),
+ highlights = title("Highlights"),
+ helptags = title("Neovim Help"),
+ manpages = title("Man Pages"),
+ lsp = {
+ title_prefix = "LSP",
+ winopts = { title_pos = "center" },
+ symbols = { title_prefix = "LSP", winopts = { title_pos = "center" } },
+ finder = title("LSP Finder"),
+ code_actions = title("Code Actions"),
+ },
+ diagnostics = title("Diagnostics"),
+ builtin = title("FzfLua Builtin"),
+ profiles = title("FzfLua Profiles"),
+ marks = title("Marks"),
+ jumps = title("Jumps"),
+ tagstack = title("Tagstack"),
+ commands = title("Commands"),
+ autocmds = title("Autocmds"),
+ command_history = title("Command history"),
+ search_history = title("Search history"),
+ registers = title("Registers"),
+ keymaps = title("Keymaps"),
+ spell_suggest = title("Spell Suggestions"),
+ filetypes = title("Filetypes"),
+ packadd = title("Packer Packadd"),
+ menus = title("Menus"),
+ tmux = title("Tmux Buffers"),
+ dap = {
+ commands = title("DAP Commands"),
+ configurations = title("DAP Configurations"),
+ variables = title("DAP Variables"),
+ frames = title("DAP Frames"),
+ breakpoints = title("DAP Breakpoints"),
+ },
+}
diff --git a/lua/fzf-lua/profiles/fzf-native.lua b/lua/fzf-lua/profiles/fzf-native.lua
index e438a582..e70d3959 100644
--- a/lua/fzf-lua/profiles/fzf-native.lua
+++ b/lua/fzf-lua/profiles/fzf-native.lua
@@ -3,6 +3,7 @@ return {
winopts = { preview = { default = "bat" } },
manpages = { previewer = "man_native" },
helptags = { previewer = "help_native" },
+ lsp = { code_actions = { previewer = "codeaction_native" } },
tags = { previewer = "bat" },
btags = { previewer = "bat" },
}
diff --git a/lua/fzf-lua/profiles/fzf-tmux.lua b/lua/fzf-lua/profiles/fzf-tmux.lua
index 94b9cd33..bc509e33 100644
--- a/lua/fzf-lua/profiles/fzf-tmux.lua
+++ b/lua/fzf-lua/profiles/fzf-tmux.lua
@@ -3,9 +3,10 @@ return {
fzf_bin = "fzf-tmux",
fzf_opts = { ["--border"] = "rounded" },
fzf_tmux_opts = { ["-p"] = "80%,90%" },
- winopts = { preview = { default = "bat", layout = "horizontal" } },
+ winopts = { preview = { default = "bat" } },
manpages = { previewer = "man_native" },
helptags = { previewer = "help_native" },
+ lsp = { code_actions = { previewer = "codeaction_native" } },
tags = { previewer = "bat" },
btags = { previewer = "bat" },
}
diff --git a/lua/fzf-lua/profiles/fzf-vim.lua b/lua/fzf-lua/profiles/fzf-vim.lua
index 673e25af..d4c3a35b 100644
--- a/lua/fzf-lua/profiles/fzf-vim.lua
+++ b/lua/fzf-lua/profiles/fzf-vim.lua
@@ -98,7 +98,15 @@ return {
["--info"] = false,
["--layout"] = false,
},
- fzf_colors = vim.g.fzf_colors,
+ fzf_colors = (function()
+ return vim.tbl_map(function(v)
+ local new_v = { v[1], { v[2] } }
+ for i = 3, #v do
+ table.insert(new_v[2], v[i])
+ end
+ return new_v
+ end, type(vim.g.fzf_colors) == "table" and vim.g.fzf_colors or {})
+ end)(),
keymap = {
builtin = {
[""] = "toggle-help",
diff --git a/lua/fzf-lua/profiles/max-perf.lua b/lua/fzf-lua/profiles/max-perf.lua
index d6a756dd..9a34b918 100644
--- a/lua/fzf-lua/profiles/max-perf.lua
+++ b/lua/fzf-lua/profiles/max-perf.lua
@@ -3,6 +3,7 @@ return {
winopts = { preview = { default = "bat" } },
manpages = { previewer = "man_native" },
helptags = { previewer = "help_native" },
+ lsp = { code_actions = { previewer = "codeaction_native" } },
tags = { previewer = "bat" },
btags = { previewer = "bat" },
files = { fzf_opts = { ["--ansi"] = false } },
diff --git a/lua/fzf-lua/profiles/telescope.lua b/lua/fzf-lua/profiles/telescope.lua
index 478abf5c..08737e31 100644
--- a/lua/fzf-lua/profiles/telescope.lua
+++ b/lua/fzf-lua/profiles/telescope.lua
@@ -6,6 +6,7 @@ local function hl_validate(hl)
end
return {
+ { "default-title" }, -- base profile
desc = "match telescope default highlights|keybinds",
fzf_opts = { ["--layout"] = "default", ["--marker"] = "+" },
winopts = {
@@ -17,21 +18,23 @@ return {
horizontal = "right:50%",
layout = "flex",
flip_columns = 120,
+ delay = 10,
+ winopts = { number = false },
},
},
hls = {
normal = hl_validate "TelescopeNormal",
border = hl_validate "TelescopeBorder",
- title = hl_validate "TelescopeTitle",
+ title = hl_validate "TelescopePromptTitle",
help_normal = hl_validate "TelescopeNormal",
help_border = hl_validate "TelescopeBorder",
preview_normal = hl_validate "TelescopeNormal",
preview_border = hl_validate "TelescopeBorder",
- preview_title = hl_validate "TelescopeTitle",
+ preview_title = hl_validate "TelescopePreviewTitle",
-- builtin preview only
cursor = hl_validate "Cursor",
- cursorline = hl_validate "TelescopePreviewLine",
- cursorlinenr = hl_validate "TelescopePreviewLine",
+ cursorline = hl_validate "TelescopeSelection",
+ cursorlinenr = hl_validate "TelescopeSelection",
search = hl_validate "IncSearch",
},
fzf_colors = {
@@ -44,6 +47,7 @@ return {
["info"] = { "fg", "TelescopeMultiSelection" },
["border"] = { "fg", "TelescopeBorder" },
["gutter"] = { "bg", "TelescopeNormal" },
+ ["query"] = { "fg", "TelescopePromptNormal" },
["prompt"] = { "fg", "TelescopePromptPrefix" },
["pointer"] = { "fg", "TelescopeSelectionCaret" },
["marker"] = { "fg", "TelescopeSelectionCaret" },
diff --git a/lua/fzf-lua/providers/buffers.lua b/lua/fzf-lua/providers/buffers.lua
index d9af3e93..9f86c656 100644
--- a/lua/fzf-lua/providers/buffers.lua
+++ b/lua/fzf-lua/providers/buffers.lua
@@ -3,7 +3,8 @@ local path = require "fzf-lua.path"
local utils = require "fzf-lua.utils"
local shell = require "fzf-lua.shell"
local config = require "fzf-lua.config"
-local make_entry = require "fzf-lua.make_entry"
+local base64 = require "fzf-lua.lib.base64"
+local devicons = require "fzf-lua.devicons"
local M = {}
@@ -24,7 +25,7 @@ local filter_buffers = function(opts, unfiltered)
local bufnrs = vim.tbl_filter(function(b)
if not vim.api.nvim_buf_is_valid(b) then
excluded[b] = true
- elseif not opts.show_unlisted and vim.fn.buflisted(b) ~= 1 then
+ elseif not opts.show_unlisted and b ~= core.CTX().bufnr and vim.fn.buflisted(b) ~= 1 then
excluded[b] = true
elseif not opts.show_unloaded and not vim.api.nvim_buf_is_loaded(b) then
excluded[b] = true
@@ -34,9 +35,9 @@ local filter_buffers = function(opts, unfiltered)
excluded[b] = true
elseif opts.no_term_buffers and utils.is_term_buffer(b) then
excluded[b] = true
- elseif opts.cwd_only and not path.is_relative(vim.api.nvim_buf_get_name(b), vim.loop.cwd()) then
+ elseif opts.cwd_only and not path.is_relative_to(vim.api.nvim_buf_get_name(b), vim.loop.cwd()) then
excluded[b] = true
- elseif opts.cwd and not path.is_relative(vim.api.nvim_buf_get_name(b), opts.cwd) then
+ elseif opts.cwd and not path.is_relative_to(vim.api.nvim_buf_get_name(b), opts.cwd) then
excluded[b] = true
end
if utils.buf_is_qf(b) then
@@ -65,8 +66,8 @@ local populate_buffer_entries = function(opts, bufnrs, tabh)
local element = {
bufnr = bufnr,
flag = flag,
- info = vim.fn.getbufinfo(bufnr)[1],
- readonly = vim.api.nvim_buf_get_option(bufnr, "readonly")
+ info = utils.getbufinfo(bufnr),
+ readonly = vim.bo[bufnr].readonly
}
-- Get the name for missing/quickfix/location list buffers
@@ -118,7 +119,7 @@ local function gen_buffer_entry(opts, buf, max_bufnr, cwd)
local flags = hidden .. readonly .. changed
local leftbr = "["
local rightbr = "]"
- local bufname = #buf.info.name > 0 and path.relative(buf.info.name, cwd or vim.loop.cwd())
+ local bufname = #buf.info.name > 0 and path.relative_to(buf.info.name, cwd or vim.loop.cwd())
if opts.filename_only then
bufname = path.basename(bufname)
end
@@ -141,18 +142,11 @@ local function gen_buffer_entry(opts, buf, max_bufnr, cwd)
local buficon = ""
local hl = ""
if opts.file_icons then
- if utils.is_term_bufname(buf.info.name) then
- -- get shell-like icon for terminal buffers
- buficon, hl = make_entry.get_devicon(buf.info.name, "sh")
- else
- local filename = path.tail(buf.info.name)
- local extension = path.extension(filename)
- buficon, hl = make_entry.get_devicon(filename, extension)
- end
- if opts.color_icons then
- -- fallback to "grey" color (#817)
- local fn = utils.ansi_codes[hl] or utils.ansi_codes["dark_grey"]
- buficon = fn(buficon)
+ buficon, hl = devicons.get_devicon(buf.info.name,
+ -- shell-like icon for terminal buffers
+ utils.is_term_bufname(buf.info.name) and "sh" or nil)
+ if hl and opts.color_icons then
+ buficon = utils.ansi_from_rgb(hl, buficon)
end
end
local max_bufnr_w = 3 + #tostring(max_bufnr) + utils.ansi_escseq_len(bufnrstr)
@@ -258,11 +252,9 @@ M.buffer_lines = function(opts)
end
bufname = path.basename(filepath)
if opts.file_icons then
- local filename = path.tail(bufname)
- local extension = path.extension(filename)
- buficon, hl = make_entry.get_devicon(filename, extension)
- if opts.color_icons then
- buficon = utils.ansi_codes[hl](buficon)
+ buficon, hl = devicons.get_devicon(bufname)
+ if hl and opts.color_icons then
+ buficon = utils.ansi_from_rgb(hl, buficon)
end
end
if not bufname or #bufname == 0 then
@@ -299,10 +291,6 @@ M.buffer_lines = function(opts)
end)()
end
- if opts.search and #opts.search > 0 then
- opts.fzf_opts["--query"] = vim.fn.shellescape(opts.search)
- end
-
opts = core.set_fzf_field_index(opts, "{3}", opts._is_skim and "{}" or "{..-2}")
core.fzf_exec(contents, opts)
@@ -384,11 +372,12 @@ M.tabs = function(opts)
function(s) return s end,
utils.ansi_codes[opts.hls.tab_marker])
- local tab_cwd_tilde_base64 = vim.base64
- and vim.base64.encode(tab_cwd_tilde)
- or tab_cwd_tilde
+ local tab_cwd_tilde_base64 = base64.encode(tab_cwd_tilde)
if not opts.current_tab_only then
- cb(string.format("%d)%s:%s%s\t%s", t, tab_cwd_tilde_base64, utils.nbsp,
+ cb(string.format("%s:%d)%s%s\t%s",
+ tab_cwd_tilde_base64,
+ t,
+ utils.nbsp,
fn_title_hl(title),
(t == core.CTX().tabnr) and fn_marker_hl(marker) or ""))
end
@@ -399,8 +388,8 @@ M.tabs = function(opts)
end
opts.sort_lastused = false
- opts._prefix = string.format("%d)%s:%s%s%s",
- t, tab_cwd_tilde_base64, utils.nbsp, utils.nbsp, utils.nbsp)
+ opts._prefix = string.format("%s:%d)%s%s%s",
+ tab_cwd_tilde_base64, t, utils.nbsp, utils.nbsp, utils.nbsp)
local tabh = vim.api.nvim_list_tabpages()[t]
local buffers = populate_buffer_entries(opts, bufnrs_flat, tabh)
for _, bufinfo in pairs(buffers) do
diff --git a/lua/fzf-lua/providers/colorschemes.lua b/lua/fzf-lua/providers/colorschemes.lua
index b2b5faa7..047288dc 100644
--- a/lua/fzf-lua/providers/colorschemes.lua
+++ b/lua/fzf-lua/providers/colorschemes.lua
@@ -1,9 +1,14 @@
local core = require "fzf-lua.core"
+local path = require "fzf-lua.path"
local utils = require "fzf-lua.utils"
local shell = require "fzf-lua.shell"
local config = require "fzf-lua.config"
local actions = require "fzf-lua.actions"
+-- For AsyncDownloadManager
+local Object = require "fzf-lua.class"
+local uv = vim.loop
+
local function get_current_colorscheme()
if vim.g.colors_name then
return vim.g.colors_name
@@ -18,7 +23,6 @@ M.colorschemes = function(opts)
opts = config.normalize_opts(opts, "colorschemes")
if not opts then return end
-
local current_colorscheme = get_current_colorscheme()
local current_background = vim.o.background
local colors = opts.colors or vim.fn.getcompletion("", "color")
@@ -34,34 +38,45 @@ M.colorschemes = function(opts)
end, colors)
end
- opts.fzf_opts["--no-multi"] = ""
+ -- make sure active colorscheme is first entry (#1045)
+ for i, c in ipairs(colors) do
+ if c == current_colorscheme then
+ table.remove(colors, i)
+ table.insert(colors, 1, c)
+ break
+ end
+ end
if opts.live_preview then
-- must add ':nohidden' or fzf ignores the preview action
opts.fzf_opts["--preview-window"] = "nohidden:right:0"
- opts.preview = shell.raw_action(function(args)
- if opts.live_preview and args then
- local colorscheme = args[1]
- vim.cmd("colorscheme " .. colorscheme)
+ opts.preview = shell.raw_action(function(sel)
+ if opts.live_preview and sel then
+ vim.cmd("colorscheme " .. sel[1])
+ if type(opts.cb_preview) == "function" then
+ opts.cb_preview(sel, opts)
+ end
end
end, nil, opts.debug)
end
- opts.fn_selected = function(selected)
+ opts.fn_selected = function(selected, o)
-- reset color scheme if live_preview is enabled
-- and nothing or non-default action was selected
if opts.live_preview and (not selected or #selected[1] > 0) then
- vim.o.background = current_background
vim.cmd("colorscheme " .. current_colorscheme)
vim.o.background = current_background
end
if selected then
- actions.act(opts.actions, selected)
+ actions.act(opts.actions, selected, o)
end
- if opts.post_reset_cb then
- opts.post_reset_cb()
+ -- setup fzf-lua's own highlight groups
+ utils.setup_highlights()
+
+ if type(opts.cb_exit) == "function" then
+ opts.cb_exit(selected, opts)
end
end
@@ -120,9 +135,393 @@ M.highlights = function(opts)
end
end
- opts.fzf_opts["--no-multi"] = ""
-
core.fzf_exec(contents, opts)
end
+
+local AsyncDownloadManager = Object:extend()
+
+function AsyncDownloadManager:new(opts)
+ self.path = opts.path
+ self.dl_status = tonumber(opts.dl_status)
+ self.max_threads = tonumber(opts.max_threads) > 0 and tonumber(opts.max_threads) or 5
+ local stat, _ = uv.fs_stat(self.path)
+ if stat and stat.type ~= "directory" then
+ utils.warn(string.format(
+ [["%s" already exists and is not a directory (type:%s)]], self.path, stat.type))
+ return
+ end
+ if not stat then
+ if vim.fn.mkdir(self.path, "p") ~= 1 then
+ utils.warn(string.format([[Unable to create cache directory "%s"]], self.path))
+ return
+ end
+ end
+ if not self:load_db(opts.db) then
+ return
+ end
+ self.job_ids = {}
+ self.job_stack = {}
+ return self
+end
+
+function AsyncDownloadManager:destruct()
+ for id, _ in pairs(self.job_ids) do
+ vim.fn.jobstop(tonumber(id))
+ end
+end
+
+function AsyncDownloadManager:jobwait_all(co)
+ local jobs = {}
+ for id, _ in pairs(self.job_ids) do
+ table.insert(jobs, tonumber(id))
+ end
+ if #jobs > 0 then
+ vim.fn.jobwait(jobs)
+ if co then coroutine.resume(co) end
+ end
+end
+
+function AsyncDownloadManager:load_db(db)
+ -- store db ref and update package params
+ self.db = db
+ for k, p in pairs(self.db or {}) do
+ if type(p.url) ~= "string" then
+ utils.warn(string.format("package %s: missing 'url'", k))
+ return false
+ end
+ if type(p.colorschemes) ~= "table" or vim.tbl_isempty(p.colorschemes) then
+ utils.warn(string.format("package %s: missing or empty 'colorschemes'", k))
+ return false
+ end
+ local github_url = "https://github.com/"
+ p.dir = p.dir or k
+ p.path = path.normalize(path.join({ self.path, p.dir }))
+ p.package = p.package or k
+ p.disp_name = p.disp_name or k
+ p.disp_url = p.disp_url or p.url
+ p.disp_url = p.disp_url:gsub("^" .. github_url, "")
+ if not p.url:match("^https://") then
+ p.url = github_url .. p.url
+ end
+ if type(p.colorschemes[1]) == "string" then
+ p.colorschemes[1] = { name = p.colorschemes[1] }
+ end
+ for i, v in ipairs(p.colorschemes) do
+ p.colorschemes[i].disp_name = v.disp_name or p.disp_name
+ if not v.name and not v.lua and not v.vim then
+ utils.warn(string.format(
+ "package %s: colorschemes[%d], must contain at least 'name|lua|vim'", k, i))
+ return false
+ end
+ end
+ self.db[k] = p
+ end
+ -- caller requested a download filter
+ if self.dl_status == 0 or self.dl_status == 1 then
+ for k, _ in pairs(self.db or {}) do
+ local downloaded = self:downloaded(k)
+ if self.dl_status == 0 and downloaded
+ or self.dl_status == 1 and not downloaded then
+ self.db[k] = nil
+ end
+ end
+ end
+ return true
+end
+
+function AsyncDownloadManager:downloaded(plugin)
+ local info = plugin and self.db[plugin]
+ if not info then return end
+ local stat = uv.fs_stat(info.path)
+ return stat and stat.type == "directory"
+end
+
+function AsyncDownloadManager:downloading(plugin)
+ local info = plugin and self.db[plugin]
+ return info and info.job_id
+end
+
+function AsyncDownloadManager:get(plugin)
+ return plugin and self.db[plugin] or nil
+end
+
+function AsyncDownloadManager:set_once_on_exit(plugin, fn)
+ if not plugin or not self.db[plugin] then return end
+ self.db[plugin].on_exit = function(...)
+ fn(...)
+ self.db[plugin].on_exit = nil
+ end
+end
+
+function AsyncDownloadManager:jobwait(plugin)
+ local info = plugin and self.db[plugin]
+ if not info or not info.job_id then return end
+ vim.fn.jobwait({ info.job_id })
+end
+
+function AsyncDownloadManager:delete(plugin)
+ if not plugin or not self.db[plugin] then return end
+ if self:downloaded(plugin) then
+ vim.fn.delete(self.db[plugin].path, "rf")
+ end
+end
+
+function AsyncDownloadManager:queue(plugin, job_args)
+ if vim.tbl_count(self.job_ids) < self.max_threads then
+ self:jobstart(plugin, job_args)
+ else
+ table.insert(self.job_stack, { plugin, job_args })
+ -- while in queue, mark plugin as "downloading"
+ self.db[plugin].job_id = true
+ end
+end
+
+function AsyncDownloadManager:dequeue()
+ if #self.job_stack > 0 then
+ local plugin, job_args = unpack(table.remove(self.job_stack, #self.job_stack))
+ self:jobstart(plugin, job_args)
+ end
+end
+
+function AsyncDownloadManager:jobstart(plugin, job_args)
+ if not plugin then return end
+ local info = plugin and self.db[plugin]
+ local msg = string.format("%s %s (%s)",
+ job_args[1][2] == "clone" and "Cloning" or "Updating", info.disp_name, info.dir)
+ local job_id
+ job_args[2] = vim.tbl_extend("keep", job_args[2] or {},
+ {
+ on_exit = function(_, rc, _)
+ utils.info(string.format("%s [job_id:%d] finished with exit code %s", plugin, job_id, rc))
+ if type(info.on_exit) == "function" then
+ -- this calls `coroutine.resume` and resumes fzf's reload input stream
+ info.on_exit(_, rc, _)
+ end
+ self.job_ids[tostring(job_id)] = nil
+ self.db[plugin].job_id = nil
+ -- dequeue the next job
+ self:dequeue()
+ end
+ })
+ job_id = vim.fn.jobstart(unpack(job_args))
+ if job_id == 0 then
+ utils.warn("jobstart: invalid args")
+ elseif job_id == -1 then
+ utils.warn(string.format([[jobstart: "%s" is not executable]], job_args[1]))
+ else
+ -- job started succesfully
+ utils.info(string.format("%s [path:%s] [job_id:%d]...",
+ msg, path.HOME_to_tilde(info.path), job_id))
+ self.job_ids[tostring(job_id)] = { plugin = plugin, args = job_args }
+ self.db[plugin].job_id = job_id
+ end
+end
+
+function AsyncDownloadManager:update(plugin)
+ local info = plugin and self.db[plugin]
+ if not info then return end
+ if self:downloaded(plugin) then
+ -- git pull
+ self:queue(plugin, {
+ ---@format disable-next
+ { "git", "pull", "--rebase" },
+ { cwd = info.path }
+ })
+ else
+ -- git clone
+ self:queue(plugin, {
+ -- { "git", "clone", "--depth=1", info.url, info.dir } -- shallow clone
+ { "git", "clone", "--filter", "tree:0", info.url, info.path } -- treeless clone
+ })
+ end
+end
+
+M.apply_awesome_theme = function(dbkey, idx, opts)
+ assert(dbkey, "colorscheme dbkey is nil")
+ assert(opts._adm, "async download manager is nil")
+ local p = opts._adm:get(dbkey)
+ assert(p, "colorscheme package is nil")
+ assert(tonumber(idx) > 0, "colorscheme index is invalid")
+ local cs = p.colorschemes[tonumber(idx)]
+ -- TODO: should we check `package.loaded[...]` before packadd?
+ local ok, out = pcall(function() vim.cmd("packadd " .. p.dir) end)
+ if not ok then
+ utils.warn(string.format("Unable to packadd %s: %s", p.dir, tostring(out)))
+ return
+ end
+ if cs.vim then
+ ok, out = pcall(vim.api.nvim_exec2, cs.vim, { output = true })
+ elseif cs.lua then
+ ok, out = pcall(function() loadstring(cs.lua)() end)
+ else
+ ok, out = pcall(function() vim.cmd("colorscheme " .. cs.name) end)
+ end
+ if not ok then
+ utils.warn(string.format("Unable to apply colorscheme %s: %s", cs.disp_name, tostring(out)))
+ end
+end
+
+M.awesome_colorschemes = function(opts)
+ opts = config.normalize_opts(opts, "awesome_colorschemes")
+ if not opts then return end
+
+ opts._cur_colorscheme = get_current_colorscheme()
+ opts._cur_background = vim.o.background
+
+ local dbfile = vim.fn.expand(opts.dbfile)
+ if not path.is_absolute(dbfile) then
+ dbfile = path.normalize(path.join({ vim.g.fzf_lua_directory, opts.dbfile })) or dbfile
+ end
+
+ local json_string = utils.read_file(dbfile)
+ if not json_string or #json_string == 0 then
+ utils.warn(string.format("Unable to load json db (%s)", opts.dbfile))
+ return
+ end
+
+ local ok, json_db = pcall(vim.json.decode, json_string)
+ if not ok then
+ utils.warn(string.format("Json decode failed: %s", json_db))
+ return
+ end
+
+ -- save a ref for action
+ opts._apply_awesome_theme = M.apply_awesome_theme
+
+ opts._packpath = type(opts.packpath) == "function"
+ and opts.packpath() or tostring(opts.packpath)
+
+ opts._adm = AsyncDownloadManager:new({
+ db = json_db,
+ dl_status = opts.dl_status,
+ max_threads = opts.max_threads,
+ path = path.join({ opts._packpath, "pack", "fzf-lua", "opt" })
+ })
+ -- Error creating cache directory
+ if not opts._adm then return end
+
+ opts.func_async_callback = false
+ opts.__fn_reload = function(_)
+ return function(cb)
+ -- use coroutine & vim.schedule to avoid
+ -- E5560: vimL function must not be called in a lua loop callback
+ coroutine.wrap(function()
+ local co = coroutine.running()
+
+ -- make sure our cache is in packpath
+ vim.opt.packpath:append(opts._packpath)
+
+ -- since resume uses deepcopy having multiple db's is going to
+ -- create all sorts of voodoo issues when running resume
+ -- HACK: find a better solution (singleton?)
+ if config.__resume_data and type(config.__resume_data.opts) == "table" then
+ config.__resume_data.opts._adm.db = opts._adm.db
+ end
+
+ local sorted = vim.tbl_keys(json_db)
+ table.sort(sorted)
+
+ for _, dbkey in ipairs(sorted) do
+ local downloaded = opts._adm:downloaded(dbkey)
+ local info = opts._adm:get(dbkey)
+ for i, cs in ipairs(info.colorschemes) do
+ if opts._adm:downloading(dbkey) then
+ -- downloading, set `on_exit` callback and wait for resume
+ opts._adm:set_once_on_exit(dbkey, function(_, _, _)
+ coroutine.resume(co)
+ end)
+ coroutine.yield()
+ end
+ vim.schedule(function()
+ local icon = not downloaded
+ and opts.icons[1] -- download icon
+ or i == 1 and opts.icons[2] -- colorscheme (package) icon
+ or opts.icons[3] -- colorscheme (variant) noicon
+ local entry = string.format("%s:%d:%s %s %s",
+ dbkey,
+ i,
+ icon,
+ cs.disp_name,
+ i == 1 and string.format("(%s)", info.disp_url) or "")
+ cb(entry, function()
+ coroutine.resume(co)
+ end)
+ end)
+ coroutine.yield()
+ end
+ end
+
+ -- done
+ cb(nil)
+ end)()
+ end
+ end
+
+ local prev_act_id
+ if opts.live_preview then
+ opts.fzf_opts["--preview-window"] = "nohidden:right:0"
+ opts.preview, prev_act_id = shell.raw_action(function(sel)
+ if opts.live_preview and sel then
+ local dbkey, idx = sel[1]:match("^(.-):(%d+):")
+ if opts._adm:downloaded(dbkey) then
+ -- some colorschemes choose a different theme based on dark|light bg
+ -- restore to the original background when interface was opened
+ -- wrap in pcall as some colorschemes have bg triggers that can fail
+ pcall(function() vim.o.background = opts._cur_background end)
+ M.apply_awesome_theme(dbkey, idx, opts)
+ if type(opts.cb_preview) == "function" then
+ opts.cb_preview(sel, opts)
+ end
+ else
+ vim.cmd("colorscheme " .. opts._cur_colorscheme)
+ vim.o.background = opts._cur_background
+ end
+ end
+ end, "{}", opts.debug)
+ end
+
+ -- build the "reload" cmd and remove '-- {+}' from the initial cmd
+ local reload, id = shell.reload_action_cmd(opts, "{+}")
+ local contents = reload:gsub("%-%-%s+{%+}$", "")
+ opts.__reload_cmd = reload
+
+ opts._fn_pre_fzf = function()
+ shell.set_protected(id)
+ if prev_act_id then
+ shell.set_protected(prev_act_id)
+ end
+ end
+
+ opts.fn_selected = function(sel, o)
+ -- do not remove our cache path from packpath
+ -- or packadd in `apply_awesome_theme` fails
+ -- vim.opt.packpath:remove(o._packpath)
+
+ -- cleanup AsyncDownloadManager
+ o._adm:destruct()
+
+ -- reset color scheme if live_preview is enabled
+ -- and nothing or non-default action was selected
+ if o.live_preview and (not sel or #sel[1] > 0) then
+ vim.cmd("colorscheme " .. o._cur_colorscheme)
+ vim.o.background = o._cur_background
+ end
+
+ if sel then
+ actions.act(o.actions, sel, o)
+ end
+
+ -- setup fzf-lua's own highlight groups
+ utils.setup_highlights()
+
+ if type(o.cb_exit) == "function" then
+ o.cb_exit(sel, o)
+ end
+ end
+
+ opts = core.set_header(opts, opts.headers or { "actions" })
+ return core.fzf_exec(contents, opts)
+end
+
return M
diff --git a/lua/fzf-lua/providers/dap.lua b/lua/fzf-lua/providers/dap.lua
index 37c07efe..399b18bb 100644
--- a/lua/fzf-lua/providers/dap.lua
+++ b/lua/fzf-lua/providers/dap.lua
@@ -1,8 +1,7 @@
local core = require "fzf-lua.core"
-local path = require "fzf-lua.path"
local utils = require "fzf-lua.utils"
+local shell = require "fzf-lua.shell"
local config = require "fzf-lua.config"
-local actions = require "fzf-lua.actions"
local make_entry = require "fzf-lua.make_entry"
local _has_dap, _dap = nil, nil
@@ -41,8 +40,6 @@ M.commands = function(opts)
end,
}
- opts.fzf_opts["--no-multi"] = ""
-
core.fzf_exec(entries, opts)
end
@@ -77,78 +74,59 @@ M.configurations = function(opts)
end,
}
- opts.fzf_opts["--no-multi"] = ""
-
core.fzf_exec(entries, opts)
end
M.breakpoints = function(opts)
- if not dap() then return end
- local dap_bps = require "dap.breakpoints"
-
opts = config.normalize_opts(opts, "dap.breakpoints")
if not opts then return end
- -- so we can have accurate info on resume
- opts.fn_pre_fzf = function()
- opts._locations = dap_bps.to_qf_list(dap_bps.get())
- end
-
- -- run once to prevent opening an empty dialog
- opts.fn_pre_fzf()
+ if not dap() then return end
+ local dap_bps = require "dap.breakpoints"
- if vim.tbl_isempty(opts._locations) then
+ if vim.tbl_isempty(dap_bps.get()) then
utils.info("Breakpoint list is empty.")
return
end
- if not opts.cwd then opts.cwd = vim.loop.cwd() end
-
- opts.actions = vim.tbl_deep_extend("keep", opts.actions or {},
- {
- ["ctrl-x"] = opts.actions and opts.actions["ctrl-x"] or
- {
- function(selected, o)
- for _, e in ipairs(selected) do
- local entry = path.entry_to_file(e, o)
- if entry.bufnr > 0 and entry.line then
- dap_bps.remove(entry.bufnr, entry.line)
- end
- end
- end,
- -- resume after bp deletion
- actions.resume
- }
- })
-
- local contents = function(cb)
- local entries = {}
- for _, entry in ipairs(opts._locations) do
- table.insert(entries, make_entry.lcol(entry, opts))
- end
-
- for i, x in ipairs(entries) do
- x = ("[%s] %s"):format(
- -- tostring(opts._locations[i].bufnr),
- utils.ansi_codes.yellow(tostring(opts._locations[i].bufnr)),
- make_entry.file(x, opts))
- if x then
- cb(x, function(err)
- if err then return end
- -- close the pipe to fzf, this
- -- removes the loading indicator in fzf
- cb(nil)
- end)
- end
+ -- display relative paths by default
+ if opts.cwd == nil then opts.cwd = vim.loop.cwd() end
+
+ opts.func_async_callback = false
+ opts.__fn_reload = opts.__fn_reload or function(_)
+ return function(cb)
+ coroutine.wrap(function()
+ local co = coroutine.running()
+ local bps = dap_bps.to_qf_list(dap_bps.get())
+ for _, b in ipairs(bps) do
+ vim.schedule(function()
+ local entry = make_entry.lcol(b, opts)
+ entry = string.format("[%s]%s%s",
+ -- tostring(opts._locations[i].bufnr),
+ utils.ansi_codes.yellow(tostring(b.bufnr)),
+ utils.nbsp,
+ make_entry.file(entry, opts))
+ cb(entry, function()
+ coroutine.resume(co)
+ end)
+ end)
+ coroutine.yield()
+ end
+ cb(nil)
+ end)()
end
- cb(nil)
end
- if opts.fzf_opts["--header"] == nil then
- opts.fzf_opts["--header"] = vim.fn.shellescape((":: %s to delete a Breakpoint")
- :format(utils.ansi_codes.yellow("")))
+ -- build the "reload" cmd and remove '-- {+}' from the initial cmd
+ local reload, id = shell.reload_action_cmd(opts, "{+}")
+ local contents = reload:gsub("%-%-%s+{%+}$", "")
+ opts.__reload_cmd = reload
+
+ opts._fn_pre_fzf = function()
+ shell.set_protected(id)
end
+ opts = core.set_header(opts, opts.headers or { "actions", "cwd" })
opts = core.set_fzf_field_index(opts, "{3}", opts._is_skim and "{}" or "{..-2}")
core.fzf_exec(contents, opts)
@@ -226,8 +204,6 @@ M.frames = function(opts)
))
end
- opts.fzf_opts["--no-multi"] = ""
-
core.fzf_exec(entries, opts)
end
diff --git a/lua/fzf-lua/providers/diagnostic.lua b/lua/fzf-lua/providers/diagnostic.lua
index 0eb35573..71551a9a 100644
--- a/lua/fzf-lua/providers/diagnostic.lua
+++ b/lua/fzf-lua/providers/diagnostic.lua
@@ -51,29 +51,43 @@ M.diagnostics = function(opts)
-- configure signs and highlights
local signs = vim.diagnostic and {
- ["Error"] = { severity = 1, default = "E", sign = "DiagnosticSignError" },
- ["Warn"] = { severity = 2, default = "W", sign = "DiagnosticSignWarn" },
- ["Info"] = { severity = 3, default = "I", sign = "DiagnosticSignInfo" },
- ["Hint"] = { severity = 4, default = "H", sign = "DiagnosticSignHint" },
+ ["Error"] = { severity = 1, default = "E", name = "DiagnosticSignError" },
+ ["Warn"] = { severity = 2, default = "W", name = "DiagnosticSignWarn" },
+ ["Info"] = { severity = 3, default = "I", name = "DiagnosticSignInfo" },
+ ["Hint"] = { severity = 4, default = "H", name = "DiagnosticSignHint" },
} or {
-- At one point or another, we'll drop support for the old LSP diag
- ["Error"] = { severity = 1, default = "E", sign = "LspDiagnosticsSignError" },
- ["Warn"] = { severity = 2, default = "W", sign = "LspDiagnosticsSignWarning" },
- ["Info"] = { severity = 3, default = "I", sign = "LspDiagnosticsSignInformation" },
- ["Hint"] = { severity = 4, default = "H", sign = "LspDiagnosticsSignHint" },
+ ["Error"] = { severity = 1, default = "E", name = "LspDiagnosticsSignError" },
+ ["Warn"] = { severity = 2, default = "W", name = "LspDiagnosticsSignWarning" },
+ ["Info"] = { severity = 3, default = "I", name = "LspDiagnosticsSignInformation" },
+ ["Hint"] = { severity = 4, default = "H", name = "LspDiagnosticsSignHint" },
}
opts.__signs = {}
for k, v in pairs(signs) do
opts.__signs[v.severity] = {}
- local sign_def = vim.fn.sign_getdefined(v.sign)
- -- can be empty when config set to (#480):
- -- vim.diagnostic.config({ signs = false })
- if vim.tbl_isempty(sign_def) then sign_def = nil end
- opts.__signs[v.severity].text =
- (not opts.diag_icons or not sign_def or not sign_def[1].text)
- and v.default or vim.trim(sign_def[1].text)
- opts.__signs[v.severity].texthl = sign_def and sign_def[1].texthl or nil
+
+ -- from vim.diagnostic
+ if utils.__HAS_NVIM_010 then
+ local sign_confs = vim.diagnostic.config().signs
+ local level = vim.diagnostic.severity[k:upper()]
+ if type(sign_confs) ~= "table" or vim.tbl_isempty(sign_confs) then sign_confs = nil end
+ opts.__signs[v.severity].text =
+ (not opts.diag_icons or not sign_confs or not sign_confs.text or not sign_confs.text[level])
+ and v.default or vim.trim(sign_confs.text[level])
+ opts.__signs[v.severity].texthl = v.name
+ else
+ local sign_def = vim.fn.sign_getdefined(v.name)
+ -- can be empty when config set to (#480):
+ -- vim.diagnostic.config({ signs = false })
+ if vim.tbl_isempty(sign_def) then sign_def = nil end
+ opts.__signs[v.severity].text =
+ (not opts.diag_icons or not sign_def or not sign_def[1].text)
+ and v.default or vim.trim(sign_def[1].text)
+ opts.__signs[v.severity].texthl = sign_def and sign_def[1].texthl or nil
+ end
+
+ -- from user config
if opts.signs and opts.signs[k] and opts.signs[k].text then
opts.__signs[v.severity].text = opts.signs[k].text
end
@@ -178,6 +192,7 @@ M.diagnostics = function(opts)
local diag_entry = preprocess_diag(diag, bufnr)
if diag_entry == nil then
coroutine.resume(co)
+ return
end
local entry = make_entry.lcol(diag_entry, opts)
entry = make_entry.file(entry, opts)
diff --git a/lua/fzf-lua/providers/files.lua b/lua/fzf-lua/providers/files.lua
index 2a424663..c89ef482 100644
--- a/lua/fzf-lua/providers/files.lua
+++ b/lua/fzf-lua/providers/files.lua
@@ -31,6 +31,8 @@ local get_files_cmd = function(opts)
command = string.format("fd %s", opts.fd_opts)
elseif vim.fn.executable("rg") == 1 then
command = string.format("rg %s", opts.rg_opts)
+ elseif utils.__IS_WINDOWS then
+ command = "dir /s/b/a:-d"
else
POSIX_find_compat(opts.find_opts)
command = string.format("find -L . %s", opts.find_opts)
@@ -44,14 +46,19 @@ M.files = function(opts)
if opts.ignore_current_file then
local curbuf = vim.api.nvim_buf_get_name(0)
if #curbuf > 0 then
- curbuf = path.relative(curbuf, opts.cwd or vim.loop.cwd())
+ curbuf = path.relative_to(curbuf, opts.cwd or vim.loop.cwd())
opts.file_ignore_patterns = opts.file_ignore_patterns or {}
table.insert(opts.file_ignore_patterns,
"^" .. utils.lua_regex_escape(curbuf) .. "$")
end
end
- opts.__ACT_TO = opts.__ACT_TO or M.files
opts.cmd = get_files_cmd(opts)
+ if utils.__IS_WINDOWS and opts.cmd:match("^dir") and not opts.cwd then
+ -- `dir` command returns absolute paths with ^M for EOL
+ -- `make_entry.file` will strip the ^M
+ -- set `opts.cwd` for relative path display
+ opts.cwd = vim.loop.cwd()
+ end
local contents = core.mt_cmd_wrapper(opts)
opts = core.set_header(opts, opts.headers or { "actions", "cwd" })
return core.fzf_exec(contents, opts)
@@ -61,28 +68,40 @@ M.args = function(opts)
opts = config.normalize_opts(opts, "args")
if not opts then return end
- opts.__fn_reload = opts.__fn_reload or function(_)
- local entries = vim.fn.execute("args")
- entries = utils.strsplit(entries, "%s\n")
+ if vim.fn.argc() == 0 then
+ utils.warn("arglist is empty.")
+ return
+ end
+ opts.func_async_callback = false
+ opts.__fn_reload = opts.__fn_reload or function(_)
return function(cb)
- -- remove the current file indicator
- -- remove all non-files
- -- local start = os.time(); for _ = 1,10000,1 do
- for _, s in ipairs(entries) do
- if s:match("^%[") then
- s = s:gsub("^%[", ""):gsub("%]$", "")
- end
- local st = vim.loop.fs_stat(s)
- if opts.files_only == false or st and st.type == "file" then
- s = make_entry.file(s, opts)
- cb(s)
+ local argc = vim.fn.argc()
+
+ -- use coroutine & vim.schedule to avoid
+ -- E5560: vimL function must not be called in a lua loop callback
+ coroutine.wrap(function()
+ local co = coroutine.running()
+
+ -- local start = os.time(); for _ = 1,10000,1 do
+ for i = 0, argc - 1 do
+ vim.schedule(function()
+ local s = vim.fn.argv(i)
+ local st = vim.loop.fs_stat(s)
+ if opts.files_only == false or st and st.type == "file" then
+ s = make_entry.file(s, opts)
+ cb(s, function()
+ coroutine.resume(co)
+ end)
+ end
+ end)
+ coroutine.yield()
end
- end
- -- end; print("took", os.time()-start, "seconds.")
+ -- end; print("took", os.time()-start, "seconds.")
- -- done
- cb(nil)
+ -- done
+ cb(nil)
+ end)()
end
end
diff --git a/lua/fzf-lua/providers/git.lua b/lua/fzf-lua/providers/git.lua
index 141c331e..f49e7675 100644
--- a/lua/fzf-lua/providers/git.lua
+++ b/lua/fzf-lua/providers/git.lua
@@ -49,8 +49,10 @@ M.status = function(opts)
local contents
if opts.multiprocess then
- -- git status does not require preprocessing
- opts.__mt_preprocess = [[return true]]
+ -- git status does not require preprocessing if not loading devicons
+ opts.__mt_preprocess = opts.file_icons
+ and [[return require("fzf-lua.devicons").load()]]
+ or [[return true]]
opts.__mt_transform = [[return require("make_entry").git_status]]
contents = core.mt_cmd_wrapper(opts)
else
@@ -104,7 +106,13 @@ M.commits = function(opts)
opts.preview_pager = opts.preview_pager()
end
if opts.preview_pager then
- opts.preview = string.format("%s | %s", opts.preview, opts.preview_pager)
+ opts.preview = string.format("%s | %s", opts.preview,
+ utils._if_win_normalize_vars(opts.preview_pager))
+ end
+ if vim.o.shell and vim.o.shell:match("fish$") then
+ -- TODO: why does fish shell refuse to pass along $COLUMNS
+ -- to delta while the same exact commands works with bcommits?
+ opts.preview = "sh -c " .. libuv.shellescape(opts.preview)
end
end
opts = core.set_header(opts, opts.headers or { "actions", "cwd" })
@@ -129,7 +137,7 @@ M.bcommits = function(opts)
end
local git_root = path.git_root(opts)
if not git_root then return end
- local file = path.relative(vim.fn.expand("%:p"), git_root)
+ local file = libuv.shellescape(path.relative_to(vim.fn.expand("%:p"), git_root))
local range
if utils.mode_is_visual() then
local _, sel = utils.get_visual_selection()
@@ -141,13 +149,14 @@ M.bcommits = function(opts)
opts.cmd = opts.cmd .. " " .. (range or file)
end
if type(opts.preview) == "string" then
- opts.preview = opts.preview:gsub("[<{]file[}>]", libuv.shellescape(file))
+ opts.preview = opts.preview:gsub("[<{]file[}>]", file)
opts.preview = path.git_cwd(opts.preview, opts)
if type(opts.preview_pager) == "function" then
opts.preview_pager = opts.preview_pager()
end
if opts.preview_pager then
- opts.preview = string.format("%s | %s", opts.preview, opts.preview_pager)
+ opts.preview = string.format("%s | %s", opts.preview,
+ utils._if_win_normalize_vars(opts.preview_pager))
end
end
opts = core.set_header(opts, opts.headers or { "actions", "cwd" })
@@ -157,7 +166,6 @@ end
M.branches = function(opts)
opts = config.normalize_opts(opts, "git.branches")
if not opts then return end
- opts.fzf_opts["--no-multi"] = ""
if opts.preview then
opts.__preview = path.git_cwd(opts.preview, opts)
opts.preview = shell.raw_preview_action_cmd(function(items)
@@ -170,6 +178,7 @@ M.branches = function(opts)
return opts.__preview:gsub("{.*}", branch)
end, nil, opts.debug)
end
+ opts.headers = opts.headers or { "cwd", "actions" }
return git_cmd(opts)
end
@@ -187,6 +196,13 @@ M.stash = function(opts)
if opts.preview then
opts.preview = path.git_cwd(opts.preview, opts)
+ if type(opts.preview_pager) == "function" then
+ opts.preview_pager = opts.preview_pager()
+ end
+ if opts.preview_pager then
+ opts.preview = string.format("%s | %s", opts.preview,
+ utils._if_win_normalize_vars(opts.preview_pager))
+ end
end
if opts.search and opts.search ~= "" then
-- search by stash content, git stash -G
diff --git a/lua/fzf-lua/providers/grep.lua b/lua/fzf-lua/providers/grep.lua
index 628c3aa1..05aaa8d9 100644
--- a/lua/fzf-lua/providers/grep.lua
+++ b/lua/fzf-lua/providers/grep.lua
@@ -7,6 +7,10 @@ local make_entry = require "fzf-lua.make_entry"
local M = {}
+---@param opts table
+---@param search_query string
+---@param no_esc boolean|number
+---@return string?
local get_grep_cmd = function(opts, search_query, no_esc)
if opts.raw_cmd and #opts.raw_cmd > 0 then
return opts.raw_cmd
@@ -16,10 +20,18 @@ local get_grep_cmd = function(opts, search_query, no_esc)
command = opts.cmd
elseif vim.fn.executable("rg") == 1 then
command = string.format("rg %s", opts.rg_opts)
+ elseif utils.__IS_WINDOWS then
+ utils.warn("Grep requires installing 'rg' on Windows.")
+ return nil
else
command = string.format("grep %s", opts.grep_opts)
end
+ -- save a copy of the command for `actions.toggle_ignore`
+ -- TODO: both `get_grep_cmd` and `get_files_cmd` need to
+ -- be reworked into a table of arguments
+ opts._cmd = command
+
if opts.rg_glob and not command:match("^rg") then
opts.rg_glob = false
utils.warn("'--glob|iglob' flags require 'rg', ignoring 'rg_glob' option.")
@@ -107,6 +119,7 @@ M.grep = function(opts)
-- get the grep command before saving the last search
-- in case the search string is overwritten by 'rg_glob'
opts.cmd = get_grep_cmd(opts, opts.search, opts.no_esc)
+ if not opts.cmd then return end
local contents = core.mt_cmd_wrapper(vim.tbl_deep_extend("force", opts,
-- query was already parsed for globs inside 'get_grep_cmd'
@@ -130,16 +143,19 @@ local function normalize_live_grep_opts(opts)
opts = config.normalize_opts(opts, "grep")
if not opts then return end
- -- we need this for 'actions.grep_lgrep`
+ -- we need this for `actions.grep_lgrep`
opts.__ACT_TO = opts.__ACT_TO or M.grep
- -- NOT NEEDED SINCE RESUME DATA REFACTOR
- -- (was used by `make_entry.set_config_section`
- -- opts.__module__ = opts.__module__ or "grep"
+ -- used by `actions.toggle_ignore', normalize_opts sets `__call_fn`
+ -- to the calling function which will resolve to this fn), we need
+ -- to deref one level up to get to `live_grep_{mt|st}`
+ opts.__call_fn = utils.__FNCREF2__()
-- prepend prompt with "*" to indicate "live" query
opts.prompt = type(opts.prompt) == "string" and opts.prompt or ""
- opts.prompt = opts.prompt:match("^%*") and opts.prompt or ("*" .. opts.prompt)
+ if opts.live_ast_prefix ~= false then
+ opts.prompt = opts.prompt:match("^%*") and opts.prompt or ("*" .. opts.prompt)
+ end
-- when using live_grep there is no "query", the prompt input
-- is a regex expression and should be saved as last "search"
@@ -151,6 +167,8 @@ local function normalize_live_grep_opts(opts)
utils.map_set(config, "__resume_data.last_query", val)
-- also store query for `fzf_resume` (#963)
utils.map_set(config, "__resume_data.opts.query", val)
+ -- store in opts for convinience in action callbacks
+ o.last_query = val
else
config.resume_set(what, val, { __resume_key = o.__resume_key })
end
@@ -181,7 +199,7 @@ local function normalize_live_grep_opts(opts)
opts.query = opts.search or ""
if opts.search and #opts.search > 0 then
-- escape unless the user requested not to
- if not (opts.no_esc) then
+ if not opts.no_esc then
opts.query = utils.rg_escape(opts.search)
end
end
@@ -220,7 +238,6 @@ M.live_grep_st = function(opts)
core.fzf_exec(nil, opts)
end
-
-- multi threaded (multi-process actually) version
M.live_grep_mt = function(opts)
opts = normalize_live_grep_opts(opts)
@@ -242,22 +259,9 @@ M.live_grep_mt = function(opts)
-- this will be replaced by the approperiate fzf
-- FIELD INDEX EXPRESSION by 'fzf_exec'
opts.cmd = get_grep_cmd(opts, core.fzf_query_placeholder, 2)
+ if not opts.cmd then return end
+
local command = core.mt_cmd_wrapper(opts)
- if command ~= opts.cmd then
- -- this means mt_cmd_wrapper wrapped the command.
- -- Since now the `rg` command is wrapped inside
- -- the shell escaped '--headless .. --cmd', we won't
- -- be able to search single quotes as it will break
- -- the escape sequence. So we use a nifty trick
- -- * replace the placeholder with {argv1}
- -- * re-add the placeholder at the end of the command
- -- * preprocess then replace it with vim.fn.argv(1)
- -- NOTE: since we cannot guarantee the positional index
- -- of arguments (#291), we use the last argument instead
- command = command:gsub(core.fzf_query_placeholder, "{argvz}")
- -- prefix the query with `--` so we can support `--fixed-strings` (#781)
- .. " -- " .. core.fzf_query_placeholder
- end
-- signal 'fzf_exec' to set 'change:reload' parameters
-- or skim's "interactive" mode (AKA "live query")
@@ -389,7 +393,7 @@ M.grep_curbuf = function(opts, lgrep)
utils.info("Rg current buffer requires file on disk")
return
else
- opts.filename = path.relative(opts.filename, vim.loop.cwd())
+ opts.filename = path.relative_to(opts.filename, vim.loop.cwd())
end
-- rg globs are meaningless here since we searching a single file
opts.rg_glob = false
diff --git a/lua/fzf-lua/providers/helptags.lua b/lua/fzf-lua/providers/helptags.lua
index 8590b392..611e3a02 100644
--- a/lua/fzf-lua/providers/helptags.lua
+++ b/lua/fzf-lua/providers/helptags.lua
@@ -52,7 +52,7 @@ local fzf_fn = function(cb)
end)()
local add_tag = function(t, fzf_cb, co)
- local tag = string.format("%-80s %s %s", hl(t.tag), t.filename, t.filepath)
+ local tag = string.format("%-80s %s%s%s", hl(t.tag), t.filename, utils.nbsp, t.filepath)
fzf_cb(tag, function()
coroutine.resume(co)
end)
@@ -93,9 +93,6 @@ end
M.helptags = function(opts)
opts = config.normalize_opts(opts, "helptags")
if not opts then return end
-
- opts.fzf_opts["--no-multi"] = ""
-
core.fzf_exec(fzf_fn, opts)
end
diff --git a/lua/fzf-lua/providers/lsp.lua b/lua/fzf-lua/providers/lsp.lua
index e889d0d9..d407f1c3 100644
--- a/lua/fzf-lua/providers/lsp.lua
+++ b/lua/fzf-lua/providers/lsp.lua
@@ -48,6 +48,9 @@ local function check_capabilities(handler, silent)
return num_clients
end
+ -- UI won't open, reset the CTX
+ core.__CTX = nil
+
if utils.tbl_isempty(clients) then
if not silent then
utils.info("LSP: no client attached")
@@ -98,7 +101,7 @@ local function location_handler(opts, cb, _, result, ctx, _)
result = vim.tbl_filter(function(x)
local item = vim.lsp.util.locations_to_items({ x }, encoding)[1]
table.insert(items, item)
- if opts.cwd_only and not path.is_relative(item.filename, opts.cwd) then
+ if opts.cwd_only and not path.is_relative_to(item.filename, opts.cwd) then
return false
end
return true
@@ -188,6 +191,18 @@ local function symbol_handler(opts, cb, _, result, _, _)
if (not opts.current_buffer_only or core.CTX().bname == entry.filename) and
(not opts.regex_filter or entry.text:match(opts.regex_filter)) then
local mbicon_align = 0
+ if opts.fn_reload and type(opts.query) == "string" and #opts.query > 0 then
+ -- highlight exact matches with `live_workspace_symbols` (#1028)
+ local sym, text = entry.text:match("^(.+%])(.*)$")
+ local pattern = "[" .. utils.lua_regex_escape(
+ opts.query:gsub("%a", function(x)
+ return string.upper(x) .. string.lower(x)
+ end)
+ ) .. "]+"
+ entry.text = sym .. text:gsub(pattern, function(x)
+ return utils.ansi_codes[opts.hls.live_sym](x)
+ end)
+ end
if M._sym2style then
local kind = entry.text:match("%[(.-)%]")
local styled = kind and M._sym2style[kind]
@@ -209,7 +224,12 @@ local function symbol_handler(opts, cb, _, result, _, _)
entry = make_entry.file(entry, opts)
if entry then
local align = 48 + mbicon_align + utils.ansi_escseq_len(symbol)
- entry = string.format("%-" .. align .. "s%s%s", symbol, utils.nbsp, entry)
+ -- TODO: string.format %-{n}s fails with align > ~100?
+ -- entry = string.format("%-" .. align .. "s%s%s", symbol, utils.nbsp, entry)
+ if align > #symbol then
+ symbol = symbol .. string.rep(" ", align - #symbol)
+ end
+ entry = symbol .. utils.nbsp .. entry
cb(opts._fmt and opts._fmt.to and opts._fmt.to(entry, opts) or entry)
end
end
@@ -516,14 +536,14 @@ end
-- see $VIMRUNTIME/lua/vim/buf.lua:pick_call_hierarchy_item()
local function gen_lsp_contents_call_hierarchy(opts)
- local lsp_params = vim.lsp.util.make_position_params(core.CTX().winid)
+ local lsp_params = opts.lsp_params or vim.lsp.util.make_position_params(core.CTX().winid)
local method = "textDocument/prepareCallHierarchy"
local res, err = vim.lsp.buf_request_sync(0, method, lsp_params, 2000)
if err then
utils.err(("Error executing '%s': %s"):format(method, err))
else
local _, response = next(res)
- if vim.tbl_isempty(response) or not response.result[1] then
+ if not response or not response.result or not response.result[1] then
if not opts.silent then
utils.info(("No %s found"):format(opts.lsp_handler.label:lower()))
end
@@ -539,7 +559,11 @@ local normalize_lsp_opts = function(opts, cfg, __resume_key)
opts = config.normalize_opts(opts, cfg, __resume_key)
if not opts then return end
- if not opts.prompt and opts.prompt_postfix then
+ -- `title_prefix` is priortized over both `prompt` and `prompt_prefix`
+ if (not opts.winopts or opts.winopts.title == nil) and opts.title_prefix then
+ utils.map_set(opts,
+ "winopts.title", string.format(" %s %s ", opts.title_prefix, opts.lsp_handler.label))
+ elseif opts.prompt == nil and opts.prompt_postfix then
opts.prompt = opts.lsp_handler.label .. (opts.prompt_postfix or "")
end
@@ -559,7 +583,10 @@ local function fzf_lsp_locations(opts, fn_contents)
if opts.force_uri == nil then opts.force_uri = true end
opts = core.set_fzf_field_index(opts)
opts = fn_contents(opts)
- if not opts.__contents then return end
+ if not opts.__contents then
+ core.__CTX = nil
+ return
+ end
return core.fzf_exec(opts.__contents, opts)
end
@@ -597,6 +624,7 @@ M.finder = function(opts)
if not opts then return end
if opts.force_uri == nil then opts.force_uri = true end
local contents = {}
+ local lsp_params = opts.lsp_params
for _, p in ipairs(opts.providers) do
local method = p[1]
if not opts._providers[method] then
@@ -606,7 +634,7 @@ M.finder = function(opts)
opts.no_autoclose = true
opts.lsp_handler = handlers[method]
opts.lsp_handler.capability = handler_capabilty(opts.lsp_handler)
- opts.lsp_params = nil -- empty out previous calls params if existed
+ opts.lsp_params = lsp_params -- reset previous calls params if existed
-- returns nil for no client attached, false for unsupported capability
-- we only abort if no client is attached
@@ -633,6 +661,7 @@ M.finder = function(opts)
end
if #contents == 0 then
utils.info("LSP: no locations found")
+ core.__CTX = nil
return
end
opts = core.set_fzf_field_index(opts)
@@ -648,9 +677,9 @@ local function gen_sym2style_map(opts)
-- style==2: ""
-- style==3: ""
local s = nil
- if tonumber(opts.symbol_style) == 1 and config._has_devicons then
+ if tonumber(opts.symbol_style) == 1 then
s = ("%s %s"):format(icon, kind)
- elseif tonumber(opts.symbol_style) == 2 and config._has_devicons then
+ elseif tonumber(opts.symbol_style) == 2 then
s = icon
elseif tonumber(opts.symbol_style) == 3 then
s = kind
@@ -699,7 +728,10 @@ M.document_symbols = function(opts)
opts.fn_pre_fzf()
end
opts = gen_lsp_contents(opts)
- if not opts.__contents then return end
+ if not opts.__contents then
+ core.__CTX = nil
+ return
+ end
return core.fzf_exec(opts.__contents, opts)
end
@@ -713,7 +745,10 @@ M.workspace_symbols = function(opts)
opts = core.set_fzf_field_index(opts)
if opts.force_uri == nil then opts.force_uri = true end
opts = gen_lsp_contents(opts)
- if not opts.__contents then return end
+ if not opts.__contents then
+ core.__CTX = nil
+ return
+ end
if opts.symbol_style or opts.symbol_fmt then
opts.fn_pre_fzf = function() gen_sym2style_map(opts) end
opts.fn_post_fzf = function() M._sym2style = nil end
@@ -731,7 +766,9 @@ M.live_workspace_symbols = function(opts)
-- prepend prompt with "*" to indicate "live" query
opts.prompt = type(opts.prompt) == "string" and opts.prompt or ""
- opts.prompt = opts.prompt:match("^%*") and opts.prompt or ("*" .. opts.prompt)
+ if opts.live_ast_prefix ~= false then
+ opts.prompt = opts.prompt:match("^%*") and opts.prompt or ("*" .. opts.prompt)
+ end
-- when using live_workspace_symbols there is no "query"
-- the prompt input is the LSP query, store as "lsp_query"
@@ -742,6 +779,8 @@ M.live_workspace_symbols = function(opts)
utils.map_set(config, "__resume_data.last_query", val)
-- also store query for `fzf_resume` (#963)
utils.map_set(config, "__resume_data.opts.query", val)
+ -- store in opts for convinience in action callbacks
+ o.last_query = val
end
opts.__resume_get = function(what, o)
return config.resume_get(
@@ -765,6 +804,7 @@ M.live_workspace_symbols = function(opts)
-- use our own
opts.func_async_callback = false
opts.fn_reload = function(query)
+ opts.query = query
opts.lsp_params = { query = query or "" }
opts = gen_lsp_contents(opts)
return opts.__contents
diff --git a/lua/fzf-lua/providers/manpages.lua b/lua/fzf-lua/providers/manpages.lua
index d2a6207e..64dc2255 100644
--- a/lua/fzf-lua/providers/manpages.lua
+++ b/lua/fzf-lua/providers/manpages.lua
@@ -1,20 +1,45 @@
local core = require "fzf-lua.core"
+local utils = require "fzf-lua.utils"
local config = require "fzf-lua.config"
+local libuv = require "fzf-lua.libuv"
local M = {}
+--- E.g. mandoc: "perlfork(1, 1p) - Perl's fork() emulation" -> "perlfork, 1"
+--- E.g. mandoc: "vsnprintf, vsprintf(3P, 3p) - format output of a stdarg argument list" -> "vsnprintf", "3P"
+--- E.g. man-db: "vsnprintf (3p) - format output of a stdarg argument list" -> "vsnprintf", "3p"
+--- @param apropos_line string a selected output line from `man -k`
+--- @return string page, string and section
+local function parse_apropos(apropos_line)
+ return apropos_line:match("^([^, (]+)[^(]*%(([^), ]*)")
+end
+--- @param apropos_line string
+--- @return string arg without shellescape
+M.manpage_vim_arg = function(apropos_line)
+ return string.format("%s(%s)", parse_apropos(apropos_line))
+end
+--- @param apropos_line string
+--- @return string arg with shellescape
+M.manpage_sh_arg = function(apropos_line)
+ local page, section = parse_apropos(apropos_line)
+ return libuv.shellescape(section) .. " " .. libuv.shellescape(page)
+end
+
M.manpages = function(opts)
opts = config.normalize_opts(opts, "manpages")
if not opts then return end
+ if utils.__IS_WINDOWS then
+ utils.warn("man is not supported on Windows.")
+ return
+ end
+
opts.fn_transform = function(x)
-- split by first occurence of ' - ' (spaced hyphen)
local man, desc = x:match("^(.-) %- (.*)$")
return string.format("%-45s %s", man, desc)
end
- opts.fzf_opts["--no-multi"] = ""
-
core.fzf_exec(opts.cmd, opts)
end
diff --git a/lua/fzf-lua/providers/module.lua b/lua/fzf-lua/providers/module.lua
index 81609cdb..a818f0a0 100644
--- a/lua/fzf-lua/providers/module.lua
+++ b/lua/fzf-lua/providers/module.lua
@@ -11,12 +11,6 @@ M.metatable = function(opts)
if not opts.metatable then opts.metatable = getmetatable("").__index end
- local prev_act = shell.action(function(args)
- -- TODO: retreive method help
- local help = ""
- return string.format("%s:%s", args[1], help)
- end, nil, opts.debug)
-
local methods = {}
for k, _ in pairs(opts.metatable) do
if not opts.metatable_exclude or opts.metatable_exclude[k] == nil then
@@ -26,9 +20,13 @@ M.metatable = function(opts)
table.sort(methods, function(a, b) return a < b end)
- opts.fzf_opts["--preview"] = prev_act
+ opts.preview = shell.raw_action(function(args)
+ -- TODO: retreive method help
+ local help = ""
+ return string.format("%s:%s", args[1], help)
+ end, nil, opts.debug)
+
opts.fzf_opts["--preview-window"] = "hidden:down:10"
- opts.fzf_opts["--no-multi"] = ""
-- builtin is excluded from global resume
-- as the behavior might confuse users (#267)
@@ -37,6 +35,8 @@ M.metatable = function(opts)
core.fzf_exec(methods, opts)
end
+---@param dir string
+---@param fn fun(fname: string, name: string, type: string)
local function ls(dir, fn)
local handle = vim.loop.fs_scandir(dir)
while handle do
@@ -71,7 +71,7 @@ M.profiles = function(opts)
local ext = path.extension(fname)
if type == "file" and ext == "lua" then
local profile = name:sub(1, #name - 4)
- local res = utils.load_profile(fname, profile, true)
+ local res = utils.load_profile_fname(fname, profile, true)
if res then
local entry = string.format("%s:%-30s%s", fname,
utils.ansi_codes.yellow(profile), res.desc or "")
diff --git a/lua/fzf-lua/providers/nvim.lua b/lua/fzf-lua/providers/nvim.lua
index 5391ccb3..0ddb6e53 100644
--- a/lua/fzf-lua/providers/nvim.lua
+++ b/lua/fzf-lua/providers/nvim.lua
@@ -1,9 +1,11 @@
local core = require "fzf-lua.core"
local path = require "fzf-lua.path"
local utils = require "fzf-lua.utils"
+local libuv = require "fzf-lua.libuv"
local shell = require "fzf-lua.shell"
local config = require "fzf-lua.config"
local make_entry = require "fzf-lua.make_entry"
+local devicons = require "fzf-lua.devicons"
local M = {}
@@ -15,14 +17,6 @@ M.commands = function(opts)
local buf_commands = vim.api.nvim_buf_get_commands(0, {})
local commands = vim.tbl_extend("force", {}, global_commands, buf_commands)
- local prev_act = shell.action(function(args)
- local cmd = args[1]
- if commands[cmd] then
- cmd = vim.inspect(commands[cmd])
- end
- return cmd
- end, nil, opts.debug)
-
local entries = {}
if opts.sort_lastused then
@@ -58,8 +52,13 @@ M.commands = function(opts)
table.sort(entries, function(a, b) return a < b end)
end
- opts.fzf_opts["--no-multi"] = ""
- opts.fzf_opts["--preview"] = prev_act
+ opts.preview = shell.raw_action(function(args)
+ local cmd = args[1]
+ if commands[cmd] then
+ cmd = vim.inspect(commands[cmd])
+ end
+ return cmd
+ end, nil, opts.debug)
core.fzf_exec(entries, opts)
end
@@ -78,16 +77,13 @@ local history = function(opts, str)
string.sub(item, finish + 1))
end
- opts.fzf_opts["--no-multi"] = ""
-
core.fzf_exec(entries, opts)
end
local arg_header = function(sel_key, edit_key, text)
sel_key = utils.ansi_codes.yellow(sel_key)
edit_key = utils.ansi_codes.yellow(edit_key)
- return vim.fn.shellescape((":: %s to %s, %s to edit")
- :format(sel_key, text, edit_key))
+ return (":: %s to %s, %s to edit"):format(sel_key, text, edit_key)
end
M.command_history = function(opts)
@@ -133,8 +129,7 @@ M.jumps = function(opts)
table.insert(entries, 1,
string.format("%6s %s %s %s", opts.h1 or "jump", "line", "col", "file/text"))
- opts.fzf_opts["--no-multi"] = ""
- opts.fzf_opts["--header-lines"] = "1"
+ opts.fzf_opts["--header-lines"] = 1
core.fzf_exec(entries, opts)
end
@@ -166,15 +161,12 @@ M.tagstack = function(opts)
local entries = {}
for i, tag in ipairs(tags) do
- local bufname = path.HOME_to_tilde(
- path.relative(tag.filename, vim.loop.cwd()))
+ local bufname = path.HOME_to_tilde(path.relative_to(tag.filename, vim.loop.cwd()))
local buficon, hl
if opts.file_icons then
- local filename = path.tail(bufname)
- local extension = path.extension(filename)
- buficon, hl = make_entry.get_devicon(filename, extension)
- if opts.color_icons then
- buficon = utils.ansi_codes[hl](buficon)
+ buficon, hl = devicons.get_devicon(bufname)
+ if hl and opts.color_icons then
+ buficon = utils.ansi_from_rgb(hl, buficon)
end
end
-- table.insert(entries, ("%s)%s[%s]%s%s%s%s:%s:%s: %s %s"):format(
@@ -192,8 +184,6 @@ M.tagstack = function(opts)
tag.text))
end
- opts.fzf_opts["--no-multi"] = ""
-
core.fzf_exec(entries, opts)
end
@@ -206,25 +196,12 @@ M.marks = function(opts)
string.format("marks %s", opts.marks and opts.marks or ""))
marks = vim.split(marks, "\n")
- --[[ local prev_act = shell.action(function (args, fzf_lines, _)
- local mark = args[1]:match("[^ ]+")
- local bufnr, lnum, _, _ = unpack(vim.fn.getpos("'"..mark))
- if vim.api.nvim_buf_is_loaded(bufnr) then
- return vim.api.nvim_buf_get_lines(bufnr, lnum, fzf_lines+lnum, false)
- else
- local name = vim.fn.expand(args[1]:match(".* (.*)"))
- if vim.fn.filereadable(name) ~= 0 then
- return vim.fn.readfile(name, "", fzf_lines)
- end
- return "UNLOADED: " .. name
- end
- end) ]]
local entries = {}
local filter = opts.marks and vim.split(opts.marks, "")
for i = #marks, 3, -1 do
local mark, line, col, text = marks[i]:match("(.)%s+(%d+)%s+(%d+)%s+(.*)")
col = tostring(tonumber(col) + 1)
- if path.starts_with_separator(text) then
+ if path.is_absolute(text) then
text = path.HOME_to_tilde(text)
end
if not filter or vim.tbl_contains(filter, mark) then
@@ -240,9 +217,20 @@ M.marks = function(opts)
table.insert(entries, 1,
string.format("%-5s %s %s %s", "mark", "line", "col", "file/text"))
- -- opts.fzf_opts['--preview'] = prev_act
- opts.fzf_opts["--no-multi"] = ""
- opts.fzf_opts["--header-lines"] = "1"
+ opts.fzf_opts["--header-lines"] = 1
+ --[[ opts.preview = shell.raw_action(function (args, fzf_lines, _)
+ local mark = args[1]:match("[^ ]+")
+ local bufnr, lnum, _, _ = unpack(vim.fn.getpos("'"..mark))
+ if vim.api.nvim_buf_is_loaded(bufnr) then
+ return vim.api.nvim_buf_get_lines(bufnr, lnum, fzf_lines+lnum, false)
+ else
+ local name = vim.fn.expand(args[1]:match(".* (.*)"))
+ if vim.fn.filereadable(name) ~= 0 then
+ return vim.fn.readfile(name, "", fzf_lines)
+ end
+ return "UNLOADED: " .. name
+ end
+ end) ]]
core.fzf_exec(entries, opts)
end
@@ -275,12 +263,6 @@ M.registers = function(opts)
reg:gsub("\n", utils.ansi_codes.magenta("\\n"))
end
- local prev_act = shell.action(function(args)
- local r = args[1]:match("%[(.*)%] ")
- local _, contents = pcall(vim.fn.getreg, r)
- return contents and register_escape_special(contents) or args[1]
- end, nil, opts.debug)
-
local entries = {}
for _, r in ipairs(registers) do
-- pcall as this could fail with:
@@ -294,8 +276,11 @@ M.registers = function(opts)
end
end
- opts.fzf_opts["--no-multi"] = ""
- opts.fzf_opts["--preview"] = prev_act
+ opts.preview = shell.raw_action(function(args)
+ local r = args[1]:match("%[(.*)%] ")
+ local _, contents = pcall(vim.fn.getreg, r)
+ return contents and register_escape_special(contents) or args[1]
+ end, nil, opts.debug)
core.fzf_exec(entries, opts)
end
@@ -360,7 +345,6 @@ M.keymaps = function(opts)
table.insert(entries, v.str)
end
- opts.fzf_opts["--no-multi"] = ""
opts.fzf_opts["--header-lines"] = "1"
-- sort alphabetically
@@ -382,8 +366,6 @@ M.spell_suggest = function(opts)
if vim.tbl_isempty(entries) then return end
- opts.fzf_opts["--no-multi"] = ""
-
core.fzf_exec(entries, opts)
end
@@ -394,8 +376,6 @@ M.filetypes = function(opts)
local entries = vim.fn.getcompletion("", "filetype")
if vim.tbl_isempty(entries) then return end
- opts.fzf_opts["--no-multi"] = ""
-
core.fzf_exec(entries, opts)
end
@@ -407,8 +387,6 @@ M.packadd = function(opts)
if vim.tbl_isempty(entries) then return end
- opts.fzf_opts["--no-multi"] = ""
-
core.fzf_exec(entries, opts)
end
@@ -441,8 +419,6 @@ M.menus = function(opts)
return
end
- opts.fzf_opts["--no-multi"] = ""
-
core.fzf_exec(entries, opts)
end
@@ -466,7 +442,7 @@ M.autocmds = function(opts)
line = info and info.linedefined or 0
end
local group = a.group_name and vim.trim(a.group_name) or " "
- local entry = string.format("%s:%d:%-28s ā %-34s ā %-18s ā %s",
+ local entry = string.format("%s:%d:|%-28s ā %-34s ā %-18s ā %s",
file, line,
utils.ansi_codes.yellow(a.event),
utils.ansi_codes.blue(group),
diff --git a/lua/fzf-lua/providers/oldfiles.lua b/lua/fzf-lua/providers/oldfiles.lua
index 59bdca32..52119ac1 100644
--- a/lua/fzf-lua/providers/oldfiles.lua
+++ b/lua/fzf-lua/providers/oldfiles.lua
@@ -9,6 +9,11 @@ M.oldfiles = function(opts)
opts = config.normalize_opts(opts, "oldfiles")
if not opts then return end
+ -- cwd implies we want `cwd_only=true`
+ if opts.cwd and opts.cwd_only == nil then
+ opts.cwd_only = true
+ end
+
local current_buffer = vim.api.nvim_get_current_buf()
local current_file = vim.api.nvim_buf_get_name(current_buffer)
local sess_tbl = {}
diff --git a/lua/fzf-lua/providers/quickfix.lua b/lua/fzf-lua/providers/quickfix.lua
index f2778642..ad8e3256 100644
--- a/lua/fzf-lua/providers/quickfix.lua
+++ b/lua/fzf-lua/providers/quickfix.lua
@@ -15,7 +15,10 @@ local quickfix_run = function(opts, cfg, locations)
if not opts.cwd then opts.cwd = vim.loop.cwd() end
for _, entry in ipairs(locations) do
- table.insert(results, make_entry.lcol(entry, opts))
+ if entry.valid == 1 or not opts.only_valid then
+ entry.text = entry.text:gsub("\r?\n", " ")
+ table.insert(results, make_entry.lcol(entry, opts))
+ end
end
local contents = function(cb)
diff --git a/lua/fzf-lua/providers/tags.lua b/lua/fzf-lua/providers/tags.lua
index edda3b38..1df47553 100644
--- a/lua/fzf-lua/providers/tags.lua
+++ b/lua/fzf-lua/providers/tags.lua
@@ -7,10 +7,15 @@ local make_entry = require "fzf-lua.make_entry"
local M = {}
local function get_tags_cmd(opts)
+ -- command already set by `ctags_autogen`?
+ if opts.cmd then return opts.cmd end
local query, filter = nil, nil
local bin, flags = nil, nil
if vim.fn.executable("rg") == 1 then
bin, flags = "rg", opts.rg_opts
+ elseif utils.__IS_WINDOWS then
+ utils.warn("Tags requires installing 'rg' on Windows.")
+ return nil, nil
else
bin, flags = "grep", opts.grep_opts
end
@@ -19,7 +24,8 @@ local function get_tags_cmd(opts)
if opts.filename and #opts.filename > 0 then
-- tags use relative paths, by now we should
-- have the correct cwd from `get_ctags_cwd`
- query = libuv.shellescape(path.relative(opts.filename, opts.cwd or vim.loop.cwd()))
+ query = libuv.shellescape(
+ utils.rg_escape(path.relative_to(opts.filename, opts.cwd or vim.loop.cwd())))
elseif opts.search and #opts.search > 0 then
filter = ([[%s -v "^!"]]):format(bin)
query = libuv.shellescape(opts.no_esc and opts.search or
@@ -29,7 +35,7 @@ local function get_tags_cmd(opts)
end
return ("%s %s %s %s"):format(
bin, flags, query,
- opts._ctags_file and vim.fn.shellescape(opts._ctags_file) or ""
+ opts._ctags_file and libuv.shellescape(opts._ctags_file) or ""
), filter
end
@@ -66,7 +72,7 @@ M._TAGS2CWD = {}
local function tags(opts)
-- make sure we have the correct 'bat' previewer for tags
- if opts.previewer == "bat_native" then opts.previewer = "bat_async" end
+ if opts.previewer == "bat_native" then opts.previewer = "bat" end
-- signal actions this is a ctag
opts._ctag = true
@@ -76,7 +82,7 @@ local function tags(opts)
-- tags file should always resolve to an absolute path, already "expanded" by
-- `get_ctags_file` we take care of the case where `opts.ctags_file = "tags"`
- if not path.starts_with_separator(opts._ctags_file) then
+ if not path.is_absolute(opts._ctags_file) then
opts._ctags_file = path.join({ opts.cwd or vim.loop.cwd(), opts.ctags_file })
end
@@ -146,7 +152,8 @@ local function tags(opts)
-- we need this for 'actions.grep_lgrep'
opts.__ACT_TO = opts.__ACT_TO or M.grep
-- live_grep requested by caller ('tags_live_grep')
- local _, filter = get_tags_cmd({ search = "dummy" })
+ local cmd, filter = get_tags_cmd({ search = "dummy" })
+ if not cmd then return end -- cmd only used for this test
opts.filter = (opts.filter == nil) and filter or opts.filter
-- rg globs are meaningless here since we are searching
-- a single file
@@ -168,7 +175,8 @@ local function tags(opts)
-- Since we cannot use include and exclude in the
-- same grep command, we need to use a pipe to filter
local cmd, filter = get_tags_cmd(opts)
- opts.raw_cmd = opts.cmd or cmd
+ if not cmd then return end
+ opts.raw_cmd = cmd
opts.filter = (opts.filter == nil) and filter or opts.filter
if opts.filter and #opts.filter > 0 then
opts.raw_cmd = ("%s | %s"):format(opts.raw_cmd, opts.filter)
diff --git a/lua/fzf-lua/providers/tmux.lua b/lua/fzf-lua/providers/tmux.lua
index 942c4573..3975c90b 100644
--- a/lua/fzf-lua/providers/tmux.lua
+++ b/lua/fzf-lua/providers/tmux.lua
@@ -14,9 +14,7 @@ M.buffers = function(opts)
return string.format("[%s] %s", utils.ansi_codes.yellow(buf), data)
end
- opts.fzf_opts["--no-multi"] = ""
- opts.fzf_opts["--delimiter"] = "'[:]'"
- opts.fzf_opts["--preview"] = shell.preview_action_cmd(function(items)
+ opts.fzf_opts["--preview"] = shell.raw_preview_action_cmd(function(items)
local buf = items[1]:match("^%[(.-)%]")
return string.format("tmux show-buffer -b %s", buf)
end, opts.debug)
diff --git a/lua/fzf-lua/providers/ui_select.lua b/lua/fzf-lua/providers/ui_select.lua
index 464c663f..af48e079 100644
--- a/lua/fzf-lua/providers/ui_select.lua
+++ b/lua/fzf-lua/providers/ui_select.lua
@@ -49,6 +49,7 @@ end
M.accept_item = function(selected, o)
local idx = selected and tonumber(selected[1]:match("^%s*(%d+)%.")) or nil
o._on_choice(idx and o._items[idx] or nil, idx)
+ o._on_choice_called = true
end
M.ui_select = function(items, ui_opts, on_choice)
@@ -73,12 +74,6 @@ M.ui_select = function(items, ui_opts, on_choice)
kind = "quickfix",
title = "Mark `mymainmenu` as defined global."
} } ]]
- -- exit visual mode if needed
- local mode = vim.api.nvim_get_mode()
- if not mode.mode:match("^n") then
- utils.feed_keys_termcodes("")
- end
-
local entries = {}
local max_bits = #(tostring(#items))
@@ -103,7 +98,7 @@ M.ui_select = function(items, ui_opts, on_choice)
end
opts.fzf_opts = vim.tbl_extend("keep", opts.fzf_opts or {}, {
- ["--no-multi"] = "",
+ ["--no-multi"] = true,
["--preview-window"] = "hidden:right:0",
})
@@ -125,9 +120,21 @@ M.ui_select = function(items, ui_opts, on_choice)
config.set_action_helpstr(opts.actions["default"], nil)
if not selected then
+ -- with `actions.dummy_abort` this doesn't get called anymore
+ -- as the action is configured as a valid fzf "accept" (thus
+ -- `selected` isn't empty), see below comment for mor info
on_choice(nil, nil)
else
+ o._on_choice_called = nil
actions.act(o.actions, selected, o)
+ if not o._on_choice_called then
+ -- see comment above, `on_choice` wasn't called, either
+ -- "dummy_abort" (ctrl-c/esc) or (unlikely) the user setup
+ -- additional binds that aren't for "accept". Not calling
+ -- with nil (no action) can cause issues, for example with
+ -- dressing.nvim (#1014)
+ on_choice(nil, nil)
+ end
end
if opts.post_action_cb then
diff --git a/lua/fzf-lua/shell.lua b/lua/fzf-lua/shell.lua
index 8416fb44..521c23ac 100644
--- a/lua/fzf-lua/shell.lua
+++ b/lua/fzf-lua/shell.lua
@@ -1,6 +1,7 @@
-- modified version of:
-- https://github.com/vijaymarupudi/nvim-fzf/blob/master/lua/fzf/actions.lua
local uv = vim.loop
+local utils = require "fzf-lua.utils"
local path = require "fzf-lua.path"
local libuv = require "fzf-lua.libuv"
@@ -53,6 +54,10 @@ function M.raw_async_action(fn, fzf_field_expression, debug)
local receiving_function = function(pipe_path, ...)
local pipe = uv.new_pipe(false)
local args = { ... }
+ -- unesacpe double backslashes on windows
+ if type(args[1]) == "table" then
+ args[1] = vim.tbl_map(function(x) return libuv.unescape_fzf(x) end, args[1])
+ end
-- save selected item in main module's __INFO
-- use loadstring to avoid circular require
pcall(function()
@@ -79,8 +84,10 @@ function M.raw_async_action(fn, fzf_field_expression, debug)
-- this is for windows WSL and AppImage users, their nvim path isn't just
-- 'nvim', it can be something else
local nvim_bin = os.getenv("FZF_LUA_NVIM_BIN") or vim.v.progpath
- local nvim_runtime = os.getenv("FZF_LUA_NVIM_BIN") and ""
- or string.format("VIMRUNTIME=%s ", libuv.shellescape(vim.env.VIMRUNTIME))
+ local nvim_runtime = os.getenv("FZF_LUA_NVIM_BIN") and "" or string.format(
+ utils._if_win([[set VIMRUNTIME=%s& ]], "VIMRUNTIME=%s "),
+ utils._if_win(path.normalize(vim.env.VIMRUNTIME),
+ libuv.shellescape(vim.env.VIMRUNTIME)))
local call_args = ("fzf_lua_server=[[%s]], fnc_id=%d %s"):format(
vim.g.fzf_lua_server, id, debug and ", debug=true" or "")
@@ -92,8 +99,8 @@ function M.raw_async_action(fn, fzf_field_expression, debug)
-- worktrees (#600)
local action_cmd = ("%s%s -n --headless --clean --cmd %s -- %s"):format(
nvim_runtime,
- libuv.shellescape(nvim_bin),
- libuv.shellescape(("lua loadfile([[%s]])().rpc_nvim_exec_lua({%s})")
+ libuv.shellescape(path.normalize(nvim_bin)),
+ libuv.shellescape(("lua vim.g.did_load_filetypes=1; loadfile([[%s]])().rpc_nvim_exec_lua({%s})")
:format(path.join { vim.g.fzf_lua_directory, "shell_helper.lua" }, call_args)),
fzf_field_expression)
@@ -102,7 +109,7 @@ end
function M.async_action(fn, fzf_field_expression, debug)
local action_string, id = M.raw_async_action(fn, fzf_field_expression, debug)
- return vim.fn.shellescape(action_string), id
+ return libuv.shellescape(action_string), id
end
function M.raw_action(fn, fzf_field_expression, debug)
@@ -136,12 +143,12 @@ end
function M.action(fn, fzf_field_expression, debug)
local action_string, id = M.raw_action(fn, fzf_field_expression, debug)
- return vim.fn.shellescape(action_string), id
+ return libuv.shellescape(action_string), id
end
M.preview_action_cmd = function(fn, fzf_field_expression, debug)
local action_string, id = M.raw_preview_action_cmd(fn, fzf_field_expression, debug)
- return vim.fn.shellescape(action_string), id
+ return libuv.shellescape(action_string), id
end
M.raw_preview_action_cmd = function(fn, fzf_field_expression, debug)
@@ -164,12 +171,17 @@ M.raw_preview_action_cmd = function(fn, fzf_field_expression, debug)
libuv.process_kill(M.__pid_preview)
M.__pid_preview = nil
- return libuv.spawn({
- cmd = fn(...),
+ local opts = fn(...)
+ if type(opts) == "string" then
+ --backward compat
+ opts = { cmd = opts }
+ end
+
+ return libuv.spawn(vim.tbl_extend("force", opts, {
cb_finish = on_finish,
cb_write = on_write,
cb_pid = function(pid) M.__pid_preview = pid end,
- }, false)
+ }))
end, fzf_field_expression, debug)
end
diff --git a/lua/fzf-lua/shell_helper.lua b/lua/fzf-lua/shell_helper.lua
index ac0e4ec4..68d4423d 100644
--- a/lua/fzf-lua/shell_helper.lua
+++ b/lua/fzf-lua/shell_helper.lua
@@ -2,8 +2,17 @@
-- https://github.com/vijaymarupudi/nvim-fzf/blob/master/action_helper.lua
local uv = vim.loop
+local _is_win = vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1
+
+---@return string
+local function windows_pipename()
+ local tmpname = vim.fn.tempname()
+ tmpname = string.gsub(tmpname, "\\", "")
+ return ([[\\.\pipe\%s]]):format(tmpname)
+end
+
local function get_preview_socket()
- local tmp = vim.fn.tempname()
+ local tmp = _is_win and windows_pipename() or vim.fn.tempname()
local socket = uv.new_pipe(false)
uv.pipe_bind(socket, tmp)
return socket, tmp
@@ -21,7 +30,7 @@ uv.listen(preview_socket, 100, function(_)
uv.close(preview_receive_socket)
uv.close(preview_socket)
vim.schedule(function()
- vim.cmd [[qall]]
+ vim.cmd([[qall]])
end)
return
end
@@ -29,13 +38,17 @@ uv.listen(preview_socket, 100, function(_)
end)
end)
-
local function rpc_nvim_exec_lua(opts)
local success, errmsg = pcall(function()
-- fzf selection is unpacked as the argument list
local fzf_selection = {}
- for i = 1, vim.fn.argc() do
- table.insert(fzf_selection, vim.fn.argv(i - 1))
+ local nargs = vim.fn.argc()
+ for i = 0, nargs - 1 do
+ -- On Windows, vim.fn.argv() normalizes the path (replaces bslash with fslash)
+ -- while vim.v.argv provides access to the raw argument, however, vim.v.argv
+ -- contains the headless wrapper arguments so we need to index backwards
+ table.insert(fzf_selection,
+ _is_win and vim.v.argv[#vim.v.argv - nargs + 1 + i] or vim.fn.argv(i))
end
-- for skim compatibility
local preview_lines = vim.env.FZF_PREVIEW_LINES or vim.env.LINES
@@ -55,7 +68,7 @@ local function rpc_nvim_exec_lua(opts)
preview_socket_path,
fzf_selection,
tonumber(preview_lines),
- tonumber(preview_cols)
+ tonumber(preview_cols),
})
vim.fn.chanclose(chan_id)
end)
@@ -67,17 +80,25 @@ local function rpc_nvim_exec_lua(opts)
for i = 1, #vim.v.argv do
io.stderr:write(("[DEBUG]\targv[%d] = %s\n"):format(i, vim.v.argv[i]))
end
- for i = 1, vim.fn.argc() do
- io.stderr:write(("[DEBUG]\targ[%d] = %s\n"):format(i, vim.fn.argv(i - 1)))
+ local nargs = vim.fn.argc()
+ for i = 0, nargs - 1 do
+ io.stderr:write(("[DEBUG]\targv[%d] = %s\n"):format(i, vim.fn.argv(i)))
+ end
+ for i = 0, nargs - 1 do
+ local argv_idx = #vim.v.argv - nargs + 1 + i
+ io.stderr:write(("[DEBUG]\tv:arg[%d:%d] = %s\n"):format(i, argv_idx, vim.v.argv[argv_idx]))
+ end
+ for _, var in ipairs({ "LINES", "COLUMNS" }) do
+ io.stderr:write(("[DEBUG]\t$%s = %s\n"):format(var, os.getenv(var) or ""))
end
end
if not success then
io.stderr:write(("FzfLua Error: %s\n"):format(errmsg or ""))
- vim.cmd [[qall]]
+ vim.cmd([[qall]])
end
end
return {
- rpc_nvim_exec_lua = rpc_nvim_exec_lua
+ rpc_nvim_exec_lua = rpc_nvim_exec_lua,
}
diff --git a/lua/fzf-lua/utils.lua b/lua/fzf-lua/utils.lua
index c5b7bc64..567e6047 100644
--- a/lua/fzf-lua/utils.lua
+++ b/lua/fzf-lua/utils.lua
@@ -8,17 +8,14 @@ end
local M = {}
+M.__HAS_NVIM_06 = vim.fn.has("nvim-0.6") == 1
M.__HAS_NVIM_07 = vim.fn.has("nvim-0.7") == 1
M.__HAS_NVIM_08 = vim.fn.has("nvim-0.8") == 1
M.__HAS_NVIM_09 = vim.fn.has("nvim-0.9") == 1
M.__HAS_NVIM_010 = vim.fn.has("nvim-0.10") == 1
-
-
--- limit devicons support to nvim >=0.8, although official support is >=0.7
--- running setup on 0.7 errs with "W18: Invalid character in group name"
-if M.__HAS_NVIM_08 then
- M.__HAS_DEVICONS = pcall(require, "nvim-web-devicons")
-end
+M.__IS_WINDOWS = vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1
+-- `:help shellslash` (for more info see #1055)
+M.__WIN_HAS_SHELLSLASH = M.__IS_WINDOWS and vim.fn.exists("+shellslash")
function M.__FILE__() return debug.getinfo(2, "S").source end
@@ -37,6 +34,11 @@ function M.__FNCREF2__()
return dbginfo and dbginfo.func
end
+function M.__FNCREF3__()
+ local dbginfo = debug.getinfo(4, "f")
+ return dbginfo and dbginfo.func
+end
+
-- sets an invisible unicode character as icon separator
-- the below was reached after many iterations, a short summary of everything
-- that was tried and why it failed:
@@ -78,10 +80,62 @@ M._if = function(bool, a, b)
end
end
+M._if_win = function(a, b)
+ if M.__IS_WINDOWS then
+ return a
+ else
+ return b
+ end
+end
+
+
+-- Substitute unix style $VAR with
+-- Style 1: %VAR%
+-- Style 2: !VAR!
+M._if_win_normalize_vars = function(cmd, style)
+ if not M.__IS_WINDOWS then return cmd end
+ local expander = style == 2 and "!" or "%"
+ cmd = cmd:gsub("%$[^%s]+", function(x) return expander .. x:sub(2) .. expander end)
+ if style == 2 then
+ -- also sub %VAR% for !VAR!
+ cmd = cmd:gsub("%%[^%s]+%%", function(x) return "!" .. x:sub(2, #x - 1) .. "!" end)
+ end
+ return cmd
+end
+
+M.shell_nop = function()
+ return M._if_win("break", "true")
+end
+
+---@param vars table
+---@return table
+M.shell_setenv_str = function(vars)
+ local ret = {}
+ for k, v in pairs(vars or {}) do
+ table.insert(ret, M._if_win(
+ string.format([[set %s=%s&&]], tostring(k), tostring(v)),
+ string.format("%s=%s;", tostring(k), tostring(v))
+ ))
+ end
+ return ret
+end
+
+---@param inputstr string
+---@param sep string
+---@return string[]
M.strsplit = function(inputstr, sep)
local t = {}
- for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do
- table.insert(t, str)
+ if #sep == 1 then
+ for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do
+ table.insert(t, str)
+ end
+ else
+ local s, m, r = inputstr, nil, nil
+ repeat
+ m, r = s:match("^(.-)" .. sep .. "(.*)$")
+ s = r and r or s
+ table.insert(t, m or s)
+ until not m
end
return t
end
@@ -96,6 +150,9 @@ M.find_last_char = function(str, c)
end
end
+---@param str string
+---@param c integer
+---@param start_idx integer
M.find_next_char = function(str, c, start_idx)
for i = start_idx or 1, #str do
if string_byte(str, i) == c then
@@ -142,13 +199,16 @@ function M.is_darwin()
return vim.loop.os_uname().sysname == "Darwin"
end
+---@param str string
+---@return string
function M.rg_escape(str)
if not str then return str end
-- [(~'"\/$?'`*&&||;[]<>)]
-- escape "\~$?*|[()^-."
- return str:gsub("[\\~$?*|{\\[()^%-%.%+]", function(x)
+ local ret = str:gsub("[\\~$?*|{\\[()^%-%.%+]", function(x)
return "\\" .. x
end)
+ return ret
end
function M.sk_escape(str)
@@ -176,7 +236,7 @@ end
function M.glob_escape(str)
if not str then return str end
- return str:gsub("[\\%{}[%]]", function(x)
+ return str:gsub("[%{}[%]]", function(x)
return [[\]] .. x
end)
end
@@ -277,7 +337,6 @@ M.read_file_async = function(filepath, callback)
end)
end
-
-- deepcopy can fail with: "Cannot deepcopy object of type userdata" (#353)
-- this can happen when copying items/on_choice params of vim.ui.select
-- run in a pcall and fallback to our poor man's clone
@@ -317,7 +376,10 @@ function M.tbl_isempty(T)
end
function M.tbl_extend(t1, t2)
- return table.move(t2, 1, #t2, #t1 + 1, t1)
+ for _, v in ipairs(t2) do
+ table.insert(t1, v)
+ end
+ return t1
end
-- Get map value from string key
@@ -340,6 +402,10 @@ end
-- Set map value for string key
-- e.g. `map_set(m, "key.sub1.sub2", value)`
-- if need be, build map tree as we go along
+---@param m table?
+---@param k string
+---@param v unknown
+---@return table
function M.map_set(m, k, v)
m = m or {}
local keys = M.strsplit(k, ".")
@@ -356,6 +422,8 @@ function M.map_set(m, k, v)
return m
end
+---@param m table?
+---@return table?
function M.map_tolower(m)
if not m then
return
@@ -367,6 +435,86 @@ function M.map_tolower(m)
return ret
end
+-- Flatten map's keys recursively
+-- { a = { a1 = ..., a2 = ... } }
+-- will be transformed to:
+-- {
+-- ["a.a1"] = ...,
+-- ["a.a2"] = ...,
+-- }
+---@param m table?
+---@return table?
+function M.map_flatten(m, prefix)
+ if vim.tbl_isempty(m) then return {} end
+ local ret = {}
+ prefix = prefix and string.format("%s.", prefix) or ""
+ for k, v in pairs(m) do
+ if type(v) == "table" and not v[1] then
+ local inner = M.map_flatten(v)
+ for ki, vi in pairs(inner) do
+ ret[prefix .. k .. "." .. ki] = vi
+ end
+ else
+ ret[prefix .. k] = v
+ end
+ end
+ return ret
+end
+
+local function hex2rgb(hexcol)
+ local r, g, b = hexcol:match("#(%x%x)(%x%x)(%x%x)")
+ if not r or not g or not b then return end
+ r, g, b = tonumber(r, 16), tonumber(g, 16), tonumber(b, 16)
+ return r, g, b
+end
+
+-- auto genreate ansi escape sequence from RGB or neovim highlights
+--[[ M.ansi_auto = setmetatable({}, {
+ -- __index metamethod only gets called when the item does not exist
+ -- we use this to auto-cache the ansi escape sequence
+ __index = function(self, k)
+ print("get", k)
+ local escseq
+ -- if not an existing highlight group lookup
+ -- in the neovim colormap and convert to RGB
+ if not k:match("^#") and vim.fn.hlexists(k) ~= 1 then
+ local col = M.COLORMAP()[k:sub(1, 1):upper() .. k:sub(2):lower()]
+ if col then
+ -- format as 6 digit hex for hex2rgb()
+ k = ("#%06x"):format(col)
+ end
+ end
+ if k:match("#%x%x%x%x%x%x") then -- index is RGB
+ -- cache the sequence as all lowercase
+ k = k:lower()
+ local v = rawget(self, k)
+ if v then return v end
+ local r, g, b = hex2rgb(k)
+ escseq = string.format("[38;2;%d;%d;%dm", r, g, b)
+ else -- index is neovim hl
+ _, escseq = M.ansi_from_hl(k, "foo")
+ print("esc", k, escseq)
+ end
+ -- We always set the item, if not RGB and hl isn't valid
+ -- create a dummy function that returns the string instead
+ local v = type(escseq) == "string" and #escseq > 0
+ and function(s)
+ if type(s) ~= "string" or #s == 0 then return "" end
+ return escseq .. s .. M.ansi_escseq.clear
+ end
+ or function(s) return s end
+ rawset(self, k, v)
+ return v
+ end,
+ __newindex = function(self, k, v)
+ assert(false,
+ string.format("modifying the ansi cache directly isn't allowed [index: %s]", k))
+ -- rawset doesn't trigger __new_index, otherwise stack overflow
+ -- we never get here but this masks the "unused local" warnings
+ rawset(self, k, v)
+ end
+}) ]]
+
M.ansi_codes = {}
M.ansi_escseq = {
-- the "\x1b" esc sequence causes issues
@@ -401,13 +549,6 @@ for color, escseq in pairs(M.ansi_escseq) do
M.cache_ansi_escseq(color, escseq)
end
-local function hex2rgb(hexcol)
- local r, g, b = hexcol:match("#(..)(..)(..)")
- if not r or not g or not b then return end
- r, g, b = tonumber(r, 16), tonumber(g, 16), tonumber(b, 16)
- return r, g, b
-end
-
-- Helper func to test for invalid (cleared) highlights
function M.is_hl_cleared(hl)
-- `vim.api.nvim_get_hl_by_name` is deprecated since v0.9.0
@@ -417,6 +558,7 @@ function M.is_hl_cleared(hl)
return true
end
else
+ ---@diagnostic disable-next-line: deprecated
local ok, hl_def = pcall(vim.api.nvim_get_hl_by_name, hl, true)
-- Not sure if this is the right way but it seems that cleared
-- highlights return 'hl_def[true] == 6' (?) and 'hl_def[true]'
@@ -462,7 +604,7 @@ end
function M.ansi_from_rgb(rgb, s)
local r, g, b = hex2rgb(rgb)
if r and g and b then
- return string.format("[38;2;%d;%d;%dm%s%s", r, g, b, s, M.ansi_escseq.clear)
+ return string.format("[38;2;%d;%d;%dm%s%s", r, g, b, s, "[0m")
end
return s
end
@@ -560,10 +702,11 @@ function M.get_visual_selection()
-- visual line doesn't provide columns
cscol, cecol = 0, 999
end
+ -- NOTE: not required since commit: e8b2093
-- exit visual mode
- vim.api.nvim_feedkeys(
- vim.api.nvim_replace_termcodes("",
- true, false, true), "n", true)
+ -- vim.api.nvim_feedkeys(
+ -- vim.api.nvim_replace_termcodes("",
+ -- true, false, true), "n", true)
else
-- otherwise, use the last known visual position
_, csrow, cscol, _ = unpack(vim.fn.getpos("'<"))
@@ -614,15 +757,15 @@ function M.reset_info()
pcall(loadstring("require'fzf-lua'.set_info(nil)"))
end
-function M.setup_highlights()
- pcall(loadstring("require'fzf-lua'.setup_highlights()"))
-end
-
-function M.setup_devicon_term_hls()
- pcall(loadstring("require'fzf-lua.make_entry'.setup_devicon_term_hls()"))
+function M.setup_highlights(override)
+ pcall(loadstring(string.format(
+ "require'fzf-lua'.setup_highlights(%s)", override and "true" or "")))
end
-function M.load_profile(fname, name, silent)
+---@param fname string
+---@param name string|nil
+---@param silent boolean
+function M.load_profile_fname(fname, name, silent)
local profile = name or fname:match("([^%p]+)%.lua$") or ""
local ok, res = pcall(dofile, fname)
if ok and type(res) == "table" then
@@ -662,7 +805,7 @@ function M.is_term_buffer(bufnr)
bufnr = bufnr == 0 and vim.api.nvim_get_current_buf() or bufnr
local winid = vim.fn.bufwinid(bufnr)
if tonumber(winid) > 0 and vim.api.nvim_win_is_valid(winid) then
- return vim.fn.getwininfo(winid)[1].terminal == 1
+ return M.getwininfo(winid).terminal == 1
end
local bufname = vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_get_name(bufnr)
return M.is_term_bufname(bufname)
@@ -670,9 +813,9 @@ end
function M.buffer_is_dirty(bufnr, warn, only_if_last_buffer)
bufnr = tonumber(bufnr) or vim.api.nvim_get_current_buf()
- local info = bufnr and vim.fn.getbufinfo(bufnr)[1]
+ local info = bufnr and M.getbufinfo(bufnr)
if info and info.changed ~= 0 then
- if only_if_last_buffer and 1 < M.tbl_length(vim.fn.win_findbuf(bufnr)) then
+ if only_if_last_buffer and 1 < #vim.fn.win_findbuf(bufnr) then
return false
end
if warn then
@@ -686,7 +829,7 @@ end
function M.save_dialog(bufnr)
bufnr = tonumber(bufnr) or vim.api.nvim_get_current_buf()
- local info = bufnr and vim.fn.getbufinfo(bufnr)[1]
+ local info = bufnr and M.getbufinfo(bufnr)
if not info.name or #info.name == 0 then
-- unnamed buffers can't be saved
M.warn(string.format("buffer %d has unsaved changes", bufnr))
@@ -710,8 +853,7 @@ end
-- 1 for qf list
-- 2 for loc list
function M.win_is_qf(winid, wininfo)
- wininfo = wininfo or
- (vim.api.nvim_win_is_valid(winid) and vim.fn.getwininfo(winid)[1])
+ wininfo = wininfo or (vim.api.nvim_win_is_valid(winid) and M.getwininfo(winid))
if wininfo and wininfo.quickfix == 1 then
return wininfo.loclist == 1 and 2 or 1
end
@@ -719,8 +861,7 @@ function M.win_is_qf(winid, wininfo)
end
function M.buf_is_qf(bufnr, bufinfo)
- bufinfo = bufinfo or
- (vim.api.nvim_buf_is_valid(bufnr) and vim.fn.getbufinfo(bufnr)[1])
+ bufinfo = bufinfo or (vim.api.nvim_buf_is_valid(bufnr) and M.getbufinfo(bufnr))
if bufinfo and bufinfo.variables and
bufinfo.variables.current_syntax == "qf" and
not vim.tbl_isempty(bufinfo.windows) then
@@ -825,15 +966,31 @@ end
-- Close a buffer without triggering an autocmd
function M.nvim_buf_delete(bufnr, opts)
- if not vim.api.nvim_buf_is_valid(bufnr) then
- return
- end
+ if not vim.api.nvim_buf_is_valid(bufnr) then return end
local save_ei = vim.o.eventignore
vim.o.eventignore = "all"
vim.api.nvim_buf_delete(bufnr, opts)
vim.o.eventignore = save_ei
end
+function M.getbufinfo(bufnr)
+ if M.__HAS_AUTOLOAD_FNS then
+ return vim.fn["fzf_lua#getbufinfo"](bufnr)
+ else
+ local info = vim.fn.getbufinfo(bufnr)
+ return info[1] or info
+ end
+end
+
+function M.getwininfo(winid)
+ if M.__HAS_AUTOLOAD_FNS then
+ return vim.fn["fzf_lua#getwininfo"](winid)
+ else
+ local info = vim.fn.getwininfo(winid)
+ return info[1] or info
+ end
+end
+
-- Backward compat 'vim.keymap.set', will probably be deprecated soon
function M.keymap_set(mode, lhs, rhs, opts)
if vim.keymap then
@@ -861,7 +1018,8 @@ end
function M.io_systemlist(cmd)
if vim.system ~= nil then -- nvim 0.10+
local proc = vim.system(cmd):wait()
- local output = proc.code == 0 and proc.stdout or proc.stderr
+ local output = (type(proc.stderr) == "string" and proc.stderr or "")
+ .. (type(proc.stdout) == "string" and proc.stdout or "")
return vim.split(output, "\n", { trimempty = true }), proc.code
else
return vim.fn.systemlist(cmd), vim.v.shell_error
@@ -874,7 +1032,8 @@ end
function M.io_system(cmd)
if vim.system ~= nil then -- nvim 0.10+
local proc = vim.system(cmd):wait()
- local output = proc.code == 0 and proc.stdout or proc.stderr
+ local output = (type(proc.stderr) == "string" and proc.stderr or "")
+ .. (type(proc.stdout) == "string" and proc.stdout or "")
return output, proc.code
else
return vim.fn.system(cmd), vim.v.shell_error
@@ -921,6 +1080,7 @@ function M.neovim_bind_to_fzf(key)
["c"] = "ctrl",
["s"] = "shift",
}
+
key = key:lower():gsub("[<>]", "")
for k, v in pairs(conv_map) do
key = key:gsub(k .. "%-", v .. "-")
@@ -950,4 +1110,11 @@ function M.find_version()
return rc == 0 and tonumber(out[1]:match("(%d+.%d+)")) or nil
end
+---@return string
+function M.windows_pipename()
+ local tmpname = vim.fn.tempname()
+ tmpname = string.gsub(tmpname, "\\", "")
+ return ([[\\.\pipe\%s]]):format(tmpname)
+end
+
return M
diff --git a/lua/fzf-lua/win.lua b/lua/fzf-lua/win.lua
index 43b00942..0f1ea84a 100644
--- a/lua/fzf-lua/win.lua
+++ b/lua/fzf-lua/win.lua
@@ -158,10 +158,10 @@ end
local strip_borderchars_hl = function(border)
local default = nil
if type(border) == "string" then
- default = config.globals.winopts._borderchars[border]
+ default = config.globals.__WINOPTS.borderchars[border]
end
if not default then
- default = config.globals.winopts._borderchars["rounded"]
+ default = config.globals.__WINOPTS.borderchars["rounded"]
end
if not border or type(border) ~= "table" or #border < 8 then
return default
@@ -182,6 +182,22 @@ local strip_borderchars_hl = function(border)
return borderchars
end
+function FzfWin:preview_splits_horizontally(winopts, winid)
+ local columns = self._o._is_fzf_tmux and self._o._is_fzf_tmux_popup and self._o._tmux_columns
+ or winopts.split and vim.api.nvim_win_get_width(winid)
+ or vim.o.columns
+ return winopts.preview.layout == "horizontal"
+ or winopts.preview.layout == "flex" and columns > winopts.preview.flip_columns
+end
+
+local function update_preview_split(winopts, winid)
+ local hsplit = FzfWin:preview_splits_horizontally(winopts, winid)
+ local preview = hsplit and winopts.preview.horizontal or winopts.preview.vertical
+ -- builtin previewer params
+ winopts.preview_pos = preview:match("[^:]+") or "right"
+ winopts.preview_size = tonumber(preview:match(":(%d+)%%")) or 50
+end
+
local normalize_winopts = function(o)
-- make a local copy of opts so we don't pollute the user's options
local winopts = utils.tbl_deep_clone(o.winopts)
@@ -250,7 +266,7 @@ local normalize_winopts = function(o)
if vim.o.ambiwidth == "double" and type(winopts.border) == "table" then
local topleft = winopts.border[1]
winopts.border = topleft
- and config.globals.winopts._border2string[topleft] or "rounded"
+ and config.globals.__WINOPTS.border2string[topleft] or "rounded"
end
-- We only allow 'none|single|double|rounded'
@@ -258,8 +274,8 @@ local normalize_winopts = function(o)
-- save the original string so we can pass it
-- to the main fzf window 'nvim_open_win' (#364)
winopts._border = winopts.border
- winopts.border = config.globals.winopts._borderchars[winopts.border] or
- config.globals.winopts._borderchars["rounded"]
+ winopts.border = config.globals.__WINOPTS.borderchars[winopts.border] or
+ config.globals.__WINOPTS.borderchars["rounded"]
end
-- Store a version of borderchars with no highlights
@@ -267,18 +283,7 @@ local normalize_winopts = function(o)
winopts.nohl_borderchars = strip_borderchars_hl(winopts.border)
-- parse preview options
- local preview
- if winopts.preview.layout == "horizontal" or
- winopts.preview.layout == "flex" and
- vim.o.columns > winopts.preview.flip_columns then
- preview = winopts.preview.horizontal
- else
- preview = winopts.preview.vertical
- end
-
- -- builtin previewer params
- winopts.preview_pos = preview:match("[^:]+") or "right"
- winopts.preview_size = tonumber(preview:match(":(%d+)%%")) or 50
+ update_preview_split(winopts, 0)
return winopts
end
@@ -297,9 +302,11 @@ function FzfWin:reset_win_highlights(win)
hl = string.format("%s%s:%s", hl and hl .. "," or "", h[1], h[2])
end
end
- vim.api.nvim_win_set_option(win, "winhighlight", hl)
+ vim.wo[win].winhighlight = hl
end
+---@param exit_code integer
+---@param fzf_bufnr integer
function FzfWin:check_exit_status(exit_code, fzf_bufnr)
-- see the comment in `FzfWin:close` for more info
if fzf_bufnr and fzf_bufnr ~= self.fzf_bufnr then
@@ -342,6 +349,9 @@ local function opt_matches(opts, key, str)
return opt and opt:match(str)
end
+---@alias FzfWin table
+---@param o table
+---@return FzfWin
function FzfWin:new(o)
if _self then
-- utils.warn("Please close fzf-lua before starting a new instance")
@@ -371,7 +381,7 @@ function FzfWin:get_winopts(win, opts)
local ret = {}
for opt, _ in pairs(opts) do
if utils.nvim_has_option(opt) then
- ret[opt] = api.nvim_win_get_option(win, opt)
+ ret[opt] = vim.wo[win][opt]
end
end
return ret
@@ -387,7 +397,7 @@ function FzfWin:set_winopts(win, opts)
-- returns an invalid (really big number insead which panics
-- when called with nvim_win_set_option, wrapping in a pcall
-- ensures this plugin still works for neovim version as low as 0.5!
- pcall(vim.api.nvim_win_set_option, win, opt, value)
+ pcall(function() vim.wo[win][opt] = value end)
end
end
end
@@ -456,7 +466,7 @@ end
function FzfWin:preview_layout()
if self.winopts.split and self.previewer_is_builtin then
- local wininfo = fn.getwininfo(self.fzf_winid)[1]
+ local wininfo = utils.getwininfo(self.fzf_winid)
-- unlike floating win popups, split windows inherit the global
-- 'signcolumn' setting which affects the available width for fzf
-- 'generate_layout' will then use the sign column available width
@@ -539,7 +549,7 @@ function FzfWin:redraw_preview_border()
vim.bo[border_buf].modifiable = true
vim.bo[border_buf].bufhidden = "wipe"
end
- api.nvim_buf_set_lines(border_buf, 0, -1, 1, lines)
+ api.nvim_buf_set_lines(border_buf, 0, -1, true, lines)
-- reset botder window highlights
if self.border_winid and vim.api.nvim_win_is_valid(self.border_winid) then
vim.fn.clearmatches(self.border_winid)
@@ -590,12 +600,12 @@ function FzfWin:redraw_preview()
-- No autocmds, can only be sent with 'nvim_open_win'
self.prev_winopts.noautocmd = true
self.border_winopts.noautocmd = true
- api.nvim_buf_set_option(tmp_buf, "bufhidden", "wipe")
+ vim.bo[tmp_buf].bufhidden = "wipe"
self.border_buf = self:redraw_preview_border()
self.preview_winid = api.nvim_open_win(tmp_buf, false, self.prev_winopts)
self.border_winid = api.nvim_open_win(self.border_buf, false, self.border_winopts)
-- nowrap border or long filenames will mess things up
- api.nvim_win_set_option(self.border_winid, "wrap", false)
+ vim.wo[self.border_winid].wrap = false
-- Add win local var for the preview|border windows
api.nvim_win_set_var(self.preview_winid, "fzf_lua_preview", true)
api.nvim_win_set_var(self.border_winid, "fzf_lua_preview", true)
@@ -787,7 +797,7 @@ function FzfWin:set_tmp_buffer()
-- issue #254, resume shows an ugly cursorline.
-- remove it, nvim_win API is better than vim.wo?
-- vim.wo[self.fzf_winid].cursorline = false
- vim.api.nvim_win_set_option(self.fzf_winid, "cursorline", false)
+ vim.wo[self.fzf_winid].cursorline = false
return self.fzf_bufnr
end
@@ -838,6 +848,8 @@ function FzfWin:create()
-- save sending bufnr/winid
self.src_bufnr = vim.api.nvim_get_current_buf()
self.src_winid = vim.api.nvim_get_current_win()
+ -- save current window layout cmd
+ self.winrestcmd = vim.fn.winrestcmd()
if self.winopts.split then
vim.cmd(self.winopts.split)
@@ -845,6 +857,7 @@ function FzfWin:create()
self.fzf_winid = vim.api.nvim_get_current_win()
-- match window options with 'nvim_open_win' style:minimal
self:set_style_minimal(self.fzf_winid)
+ update_preview_split(self.winopts, self.fzf_winid)
else
-- draw the main window
self:redraw_main()
@@ -860,6 +873,10 @@ function FzfWin:create()
self:reset_win_highlights(self.fzf_winid)
+ -- potential workarond for `` freezing neovim (#1091)
+ -- https://github.com/neovim/neovim/issues/20726
+ vim.wo[self.fzf_winid].foldmethod = "manual"
+
if self.winopts.on_create and
type(self.winopts.on_create) == "function" then
self.winopts.on_create()
@@ -936,6 +953,23 @@ function FzfWin:close(fzf_bufnr)
and vim.api.nvim_win_is_valid(self.src_winid) then
vim.api.nvim_set_current_win(self.src_winid)
end
+ if self.winopts.split then
+ -- remove all windows from the restore cmd that have been closed in the meantime
+ -- if we're not doing this the result might be all over the place
+ local winnrs = vim.tbl_map(function(win)
+ return vim.api.nvim_win_get_number(win) .. ""
+ end, vim.api.nvim_tabpage_list_wins(0))
+
+ local cmd = {}
+ for cmd_part in string.gmatch(self.winrestcmd, "[^|]+") do
+ local winnr = cmd_part:match("(.)resize")
+ if vim.tbl_contains(winnrs, winnr) then
+ table.insert(cmd, cmd_part)
+ end
+ end
+
+ vim.cmd(table.concat(cmd, "|"))
+ end
if self.hls_on_close then
-- restore search highlighting if we disabled it
-- use `vim.o.hlsearch` as `vim.cmd("hls")` is invalid
@@ -959,7 +993,7 @@ function FzfWin.win_leave()
self._previewer:win_leave()
end
if not self or self.closing then return end
- _self:close()
+ self:close()
end
function FzfWin:update_scrollbar_border(o)
@@ -1009,7 +1043,7 @@ function FzfWin:update_scrollbar_border(o)
end
lines[i] = fn.strcharpart(line, 0, linew - 1) .. bar_char
end
- api.nvim_buf_set_lines(self.border_buf, 1, -2, 0, lines)
+ api.nvim_buf_set_lines(self.border_buf, 1, -2, false, lines)
-- border highlights
if hl_f or hl_e then
@@ -1073,8 +1107,8 @@ function FzfWin:update_scrollbar_float(o)
self._sbuf1 = ensure_tmp_buf(self._sbuf1)
self._swin1 = vim.api.nvim_open_win(self._sbuf1, false, style1)
local hl = self.hls.scrollfloat_e or "PmenuSbar"
- vim.api.nvim_win_set_option(self._swin1, "winhighlight",
- ("Normal:%s,NormalNC:%s,NormalFloat:%s"):format(hl, hl, hl))
+ vim.wo[self._swin1].winhighlight =
+ ("Normal:%s,NormalNC:%s,NormalFloat:%s"):format(hl, hl, hl)
end
local style2 = utils.tbl_deep_clone(style1)
style2.height = o.bar_height
@@ -1087,8 +1121,8 @@ function FzfWin:update_scrollbar_float(o)
self._sbuf2 = ensure_tmp_buf(self._sbuf2)
self._swin2 = vim.api.nvim_open_win(self._sbuf2, false, style2)
local hl = self.hls.scrollfloat_f or "PmenuThumb"
- vim.api.nvim_win_set_option(self._swin2, "winhighlight",
- ("Normal:%s,NormalNC:%s,NormalFloat:%s"):format(hl, hl, hl))
+ vim.wo[self._swin2].winhighlight =
+ ("Normal:%s,NormalNC:%s,NormalFloat:%s"):format(hl, hl, hl)
end
end
end
@@ -1110,7 +1144,7 @@ function FzfWin:update_scrollbar(hide)
local buf = api.nvim_win_get_buf(self.preview_winid)
local o = {}
- o.wininfo = fn.getwininfo(self.preview_winid)[1]
+ o.wininfo = utils.getwininfo(self.preview_winid)
o.line_count = api.nvim_buf_line_count(buf)
local topline, height = o.wininfo.topline, o.wininfo.height
@@ -1148,7 +1182,7 @@ function FzfWin:update_title(title)
end
local right_pad = 7
local border_buf = api.nvim_win_get_buf(self.border_winid)
- local top = api.nvim_buf_get_lines(border_buf, 0, 1, 0)[1]
+ local top = api.nvim_buf_get_lines(border_buf, 0, 1, false)[1]
local width = fn.strwidth(top)
if #title > width - right_pad then
title = title:sub(1, width - right_pad) .. " "
@@ -1163,7 +1197,7 @@ function FzfWin:update_title(title)
local suffix = fn.strcharpart(top, width_title + fn.strwidth(prefix), width)
local line = ("%s%s%s"):format(prefix, title, suffix)
- api.nvim_buf_set_lines(border_buf, 0, 1, 1, { line })
+ api.nvim_buf_set_lines(border_buf, 0, 1, true, { line })
if self.hls.preview_title and #title > 0 then
pcall(vim.api.nvim_win_call, self.border_winid, function()
@@ -1209,9 +1243,9 @@ end
function FzfWin.toggle_preview_wrap()
if not _self or not _self:validate_preview() then return end
local self = _self
- self.preview_wrap = not api.nvim_win_get_option(self.preview_winid, "wrap")
+ self.preview_wrap = not vim.wo[self.preview_winid].wrap
if self and self:validate_preview() then
- api.nvim_win_set_option(self.preview_winid, "wrap", self.preview_wrap)
+ vim.wo[self.preview_winid].wrap = self.preview_wrap
end
end
@@ -1407,15 +1441,16 @@ function FzfWin.toggle_help()
end
self.km_bufnr = vim.api.nvim_create_buf(false, true)
- vim.api.nvim_buf_set_option(self.km_bufnr, "bufhidden", "wipe")
+ vim.bo[self.km_bufnr].bufhidden = "wipe"
self.km_winid = vim.api.nvim_open_win(self.km_bufnr, false, winopts)
vim.api.nvim_buf_set_name(self.km_bufnr, "_FzfLuaHelp")
- vim.api.nvim_win_set_option(self.km_winid, "winhl",
- string.format("Normal:%s,FloatBorder:%s", opts.normal_hl, opts.border_hl))
- vim.api.nvim_win_set_option(self.km_winid, "winblend", opts.winblend)
- vim.api.nvim_win_set_option(self.km_winid, "foldenable", false)
- vim.api.nvim_win_set_option(self.km_winid, "wrap", false)
- vim.api.nvim_buf_set_option(self.km_bufnr, "filetype", "help")
+ vim.wo[self.km_winid].winhl =
+ string.format("Normal:%s,FloatBorder:%s", opts.normal_hl, opts.border_hl)
+ vim.wo[self.km_winid].winblend = opts.winblend
+ vim.wo[self.km_winid].foldenable = false
+ vim.wo[self.km_winid].wrap = false
+ vim.wo[self.km_winid].spell = false
+ vim.bo[self.km_bufnr].filetype = "help"
vim.cmd(string.format(
"autocmd BufLeave ++once lua %s",
diff --git a/plugin/fzf-lua.lua b/plugin/fzf-lua.lua
new file mode 100644
index 00000000..86f48a79
--- /dev/null
+++ b/plugin/fzf-lua.lua
@@ -0,0 +1,91 @@
+if vim.g.loaded_fzf_lua == 1 then
+ return
+end
+vim.g.loaded_fzf_lua = 1
+
+-- Should never be called, below nvim 0.7 "plugin/fzf-lua.vim"
+-- sets `vim.g.loaded_fzf_lua=1`
+if vim.fn.has("nvim-0.7") ~= 1 then
+ vim.api.nvim_err_writeln("Fzf-lua minimum requirement is Neovim versions 0.5")
+ return
+end
+
+vim.api.nvim_create_user_command("FzfLua", function(opts)
+ require("fzf-lua.cmd").load_command(unpack(opts.fargs))
+end, {
+ nargs = "*",
+ complete = function(_, line)
+ local metatable = require("fzf-lua")
+ local builtin_list = vim.tbl_filter(function(k)
+ return metatable._excluded_metamap[k] == nil
+ end, vim.tbl_keys(metatable))
+
+ local l = vim.split(line, "%s+")
+ local n = #l - 2
+
+ if n == 0 then
+ local commands = vim.tbl_flatten({ builtin_list })
+ table.sort(commands)
+
+ return vim.tbl_filter(function(val)
+ return vim.startswith(val, l[2])
+ end, commands)
+ end
+
+ -- Not all commands have their opts under the same key
+ local function cmd2key(cmd)
+ local cmd2cfg = {
+ {
+ patterns = { "^git_", "^dap", "^tmux_" },
+ transform = function(c) return c:gsub("_", ".") end
+ },
+ {
+ patterns = { "^lsp_code_actions$" },
+ transform = function(_) return "lsp.code_actions" end
+ },
+ { patterns = { "^lsp_.*_symbols$" }, transform = function(_) return "lsp.symbols" end },
+ { patterns = { "^lsp_" }, transform = function(_) return "lsp" end },
+ { patterns = { "^diagnostics_" }, transform = function(_) return "dianostics" end },
+ { patterns = { "^tags" }, transform = function(_) return "tags" end },
+ { patterns = { "grep" }, transform = function(_) return "grep" end },
+ { patterns = { "^complete_bline$" }, transform = function(_) return "complete_line" end },
+ }
+ for _, v in pairs(cmd2cfg) do
+ for _, p in ipairs(v.patterns) do
+ if cmd:match(p) then return v.transform(cmd) end
+ end
+ end
+ return cmd
+ end
+
+ local utils = require("fzf-lua.utils")
+ local defaults = require("fzf-lua.defaults").defaults
+ local cmd_opts = utils.map_get(defaults, cmd2key(l[2])) or {}
+ local opts = vim.tbl_filter(function(k)
+ return not k:match("^_")
+ end, vim.tbl_keys(utils.map_flatten(cmd_opts)))
+
+ -- Add globals recursively, e.g. `winopts.fullscreen`
+ -- will be later retrieved using `utils.map_get(...)`
+ for k, v in pairs({
+ winopts = false,
+ keymap = false,
+ fzf_opts = false,
+ fzf_tmux_opts = false,
+ __HLS = "hls", -- rename prefix
+ }) do
+ opts = vim.tbl_flatten({ opts, vim.tbl_keys(utils.map_flatten(defaults[k] or {}, v or k)) })
+ end
+
+ -- Add generic options that apply to all pickers
+ for _, o in ipairs({ "query" }) do
+ table.insert(opts, o)
+ end
+
+ table.sort(opts)
+
+ return vim.tbl_filter(function(val)
+ return vim.startswith(val, l[#l])
+ end, opts)
+ end,
+})
diff --git a/plugin/fzf-lua.vim b/plugin/fzf-lua.vim
index 1fd812af..2b6282f0 100644
--- a/plugin/fzf-lua.vim
+++ b/plugin/fzf-lua.vim
@@ -1,3 +1,8 @@
+" Neovim >= v0.7 uses 'plugin/fzf-lua.lua'
+if has('nvim-0.7')
+ finish
+end
+
if !has('nvim-0.5')
echohl Error
echomsg "Fzf-lua is only available for Neovim versions 0.5 and above"
diff --git a/scripts/headless_fd.sh b/scripts/headless_fd.sh
new file mode 100755
index 00000000..9505d260
--- /dev/null
+++ b/scripts/headless_fd.sh
@@ -0,0 +1,158 @@
+#!/bin/sh
+
+BASEDIR=$(cd "$(dirname "$0")" ; pwd -P)
+
+usage() {
+ echo "Usage: $0 [options]"
+ echo ""
+ echo "Options"
+ echo "-h, --help Show this help"
+ echo "-d, --debug Debug level [0|1|2|false|true|v]"
+ echo ""
+ echo "Display Options"
+ echo "-c, --cwd Working Directory"
+ echo "-x, --cmd Executed Command (default: fd --color=never)"
+ echo "-g, --git-icons Git icons [0|1|false|true] (default:false)"
+ echo "-f, --file-icons File icons [0|1|false|true] (default:true)"
+ echo "--color Color icons [0|1|false|true] (default:true)"
+}
+
+# saner programming env: these switches turn some bugs into errors
+set -o noclobber -o nounset
+
+# -allow a command to fail with !ās side effect on errexit
+# -use return value from ${PIPESTATUS[0]}, because ! hosed $?
+getopt --test > /dev/null
+if [ $? -ne 4 ]; then
+ echo '`getopt --test` failed in this environment.'
+ exit 1
+fi
+
+OPTIONS=hd:c:x:f:g:
+LONGOPTS=help,debug:,cwd:,file-icons:,git-icons:,color:,cmd:
+
+PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@")
+if [ $? -ne 0 ]; then
+ # e.g. return value is 1
+ # then getopt has complained about wrong arguments to stdout
+ usage;
+ exit 2
+fi
+# read getoptās output this way to handle the quoting right:
+eval set -- "$PARSED"
+
+debug="false"
+cwd= cmd=
+git_icons="false"
+file_icons="true"
+color_icons="true"
+
+# now enjoy the options in order and nicely split until we see --
+while true; do
+ case "$1" in
+ -h|--help)
+ usage;
+ exit 0
+ ;;
+ -d|--debug)
+ case $2 in
+ "2"|"v"|"verbose")
+ debug="v"
+ ;;
+ "1"|"true")
+ debug="true"
+ ;;
+ *)
+ debug="false"
+ ;;
+ esac
+ shift 2
+ ;;
+ -c|--cwd)
+ cwd="$2"
+ shift 2
+ ;;
+ -x|--cmd)
+ cmd="$2"
+ shift 2
+ ;;
+ -f|--file-icons)
+ case $2 in
+ "0"|"false")
+ file_icons="false"
+ ;;
+ *)
+ file_icons="true"
+ ;;
+ esac
+ shift 2
+ ;;
+ -g|--git-icons)
+ case $2 in
+ "0"|"false")
+ git_icons="false"
+ ;;
+ *)
+ git_icons="true"
+ ;;
+ esac
+ shift 2
+ ;;
+ --color)
+ case $2 in
+ "0"|"false")
+ color_icons="false"
+ ;;
+ *)
+ color_icons="true"
+ ;;
+ esac
+ shift 2
+ ;;
+ --)
+ shift
+ break
+ ;;
+ *)
+ # never get here!
+ echo "error: error while parsing command line arguments"
+ usage;
+ exit 3
+ ;;
+ esac
+done
+
+# handle non-option arguments
+if [ $# -gt 0 ]; then
+ echo "error: unrecgonized option"
+ usage;
+ exit 4
+fi
+
+VIMRUNTIME=/usr/share/nvim/runtime \
+/usr/bin/nvim -n --headless --clean --cmd "lua vim.g.did_load_filetypes=1; loadfile(
+ [[${BASEDIR}/../lua/fzf-lua/libuv.lua]])().spawn_stdio(
+ -- opts
+ {
+ g = {
+ --_fzf_lua_server = [[/run/user/1000/fzf-lua.1710687343.12851.1]],
+ _devicons_path = [[${XDG_DATA_HOME:-$HOME/.local/share}/nvim/lazy/nvim-web-devicons]],
+ _devicons_setup = [[${XDG_CONFIG_HOME:-$HOME/.config}/nvim/lua/plugins/devicons/setup.lua]],
+ },
+ _base64 = false,
+ debug = [[$debug]] == [[v]] and [[v]] or $debug,
+ file_icons = ${file_icons},
+ git_icons = ${git_icons},
+ color_icons = ${color_icons},
+ cmd = [[${cmd:-fd --color=never}]],
+ cwd = vim.fn.expand([[${cwd:-$BASEDIR}]]),
+ },
+ -- fn_transform
+ [==[
+ return require(\"make_entry\").file
+ ]==],
+ -- fn_preprocess
+ [==[
+ return require(\"make_entry\").preprocess
+ ]==]
+)"
diff --git a/tests/devicons_spec.lua b/tests/devicons_spec.lua
new file mode 100644
index 00000000..007935c6
--- /dev/null
+++ b/tests/devicons_spec.lua
@@ -0,0 +1,107 @@
+local fzf = require("fzf-lua")
+local path = fzf.path
+
+describe("Testing devicons module", function()
+ -- add devicons path from lazy to runtime so our module can load nvim-web-devicons
+ local devicons_path = path.join({ vim.fn.stdpath("data"), "lazy", "nvim-web-devicons" })
+ vim.opt.runtimepath:append(devicons_path)
+ local theme = require("nvim-web-devicons.icons-default")
+ local devicons = require("fzf-lua.devicons")
+ devicons.load()
+ -- remove from runtime so we can test the headless runtime append
+ vim.opt.runtimepath:remove(devicons_path)
+
+ it(string.format("load_icons (%s)", vim.g.fzf_lua_server), function()
+ _G._devicons_path = nil
+ _G._fzf_lua_server = vim.g.fzf_lua_server
+ vim.g.fzf_lua_is_headless = true
+ devicons.load()
+ local state = devicons.STATE
+ local icons = devicons.STATE.icons
+ assert.are.same(state.default_icon, { icon = "ļ¶", color = "#6d8086" })
+ assert.are.same(state.dir_icon, { icon = "ļ", color = nil })
+ assert.is.True(vim.tbl_count(icons.ext_has_2part) > 4)
+ assert.is.True(vim.tbl_count(icons.by_ext_2part) > 8)
+ assert.are.equal(vim.tbl_count(icons.by_filename), vim.tbl_count(theme.icons_by_filename))
+ assert.are.equal(vim.tbl_count(icons.by_ext) + vim.tbl_count(icons.by_ext_2part),
+ vim.tbl_count(theme.icons_by_file_extension))
+ end)
+ it("get_icons (headless: devicons path)", function()
+ _G._devicons_path = devicons_path
+ _G._fzf_lua_server = nil
+ vim.g.fzf_lua_is_headless = true
+ devicons.unload()
+ devicons.load()
+ local state = devicons.STATE
+ local icons = devicons.STATE.icons
+ assert.are.same(state.default_icon, { icon = "ļ¶", color = "#6d8086" })
+ assert.are.same(state.dir_icon, { icon = "ļ", color = nil })
+ assert.is.True(vim.tbl_count(icons.ext_has_2part) > 4)
+ assert.is.True(vim.tbl_count(icons.by_ext_2part) > 8)
+ assert.are.equal(vim.tbl_count(icons.by_filename), vim.tbl_count(theme.icons_by_filename))
+ assert.are.equal(vim.tbl_count(icons.by_ext) + vim.tbl_count(icons.by_ext_2part),
+ vim.tbl_count(theme.icons_by_file_extension))
+ end)
+ it("get_icons (main thread)", function()
+ _G._devicons_path = nil
+ _G._fzf_lua_server = nil
+ vim.g.fzf_lua_is_headless = nil
+ devicons.unload()
+ devicons.load()
+ local state = devicons.STATE
+ local icons = devicons.STATE.icons
+ assert.are.same(state.default_icon, { icon = "ļ¶", color = "#6d8086" })
+ assert.are.same(state.dir_icon, { icon = "ļ", color = nil })
+ assert.is.True(vim.tbl_count(icons.ext_has_2part) > 4)
+ assert.is.True(vim.tbl_count(icons.by_ext_2part) > 8)
+ assert.are.equal(vim.tbl_count(icons.by_filename), vim.tbl_count(theme.icons_by_filename))
+ assert.are.equal(vim.tbl_count(icons.by_ext) + vim.tbl_count(icons.by_ext_2part),
+ vim.tbl_count(theme.icons_by_file_extension))
+ end)
+ it("get_icon (dark)", function()
+ vim.o.background = "dark"
+ devicons.load()
+ assert.are.same({ devicons.get_devicon("") }, { "ļ¶", "#6d8086" })
+ assert.are.same({ devicons.get_devicon(".") }, { "ļ¶", "#6d8086" })
+ assert.are.same({ devicons.get_devicon("f.abc") }, { "ļ¶", "#6d8086" })
+ assert.are.same({ devicons.get_devicon("f.") }, { "ļ¶", "#6d8086" })
+ assert.are.same({ devicons.get_devicon(".f") }, { "ļ¶", "#6d8086" })
+ assert.are.same({ devicons.get_devicon("foo") }, { "ļ¶", "#6d8086" })
+ assert.are.same({ devicons.get_devicon("foo/") }, { "ļ", nil })
+ -- by filename
+ assert.are.same({ devicons.get_devicon(".editorconfig") }, { "ī", "#fff2f2" })
+ assert.are.same({ devicons.get_devicon("/path/.bashrc") }, { "ī", "#89e051" })
+ -- by 2-part extension
+ assert.are.same({ devicons.get_devicon("foo.bar.jsx") }, { "ī„", "#20c2e3" })
+ assert.are.same({ devicons.get_devicon("foo.spec.jsx") }, { "ļ", "#20c2e3" })
+ assert.are.same({ devicons.get_devicon("foo.config.ru") }, { "ī", "#701516" })
+ -- by 1-part extensions
+ assert.are.same({ devicons.get_devicon("foo.lua") }, { "ī ", "#51a0cf" })
+ assert.are.same({ devicons.get_devicon("foo.py") }, { "ī", "#ffbc03" })
+ assert.are.same({ devicons.get_devicon("foo.r") }, { "ó°", "#2266ba" })
+ assert.are.same({ devicons.get_devicon("foo.R") }, { "ó°", "#2266ba" })
+ end)
+ it("get_icon (light)", function()
+ vim.o.background = "light"
+ devicons.load()
+ assert.are.same({ devicons.get_devicon("") }, { "ļ¶", "#6d8086" })
+ assert.are.same({ devicons.get_devicon(".") }, { "ļ¶", "#6d8086" })
+ assert.are.same({ devicons.get_devicon("f.abc") }, { "ļ¶", "#6d8086" })
+ assert.are.same({ devicons.get_devicon("f.") }, { "ļ¶", "#6d8086" })
+ assert.are.same({ devicons.get_devicon(".f") }, { "ļ¶", "#6d8086" })
+ assert.are.same({ devicons.get_devicon("foo") }, { "ļ¶", "#6d8086" })
+ assert.are.same({ devicons.get_devicon("foo/") }, { "ļ", nil })
+ -- by filename
+ assert.are.same({ devicons.get_devicon(".editorconfig") }, { "ī", "#333030" })
+ assert.are.same({ devicons.get_devicon("/path/.bashrc") }, { "ī", "#447028" })
+ -- by 2-part extension
+ assert.are.same({ devicons.get_devicon("foo.bar.jsx") }, { "ī„", "#158197" })
+ assert.are.same({ devicons.get_devicon("foo.spec.jsx") }, { "ļ", "#158197" })
+ assert.are.same({ devicons.get_devicon("foo.config.ru") }, { "ī", "#701516" })
+ -- by 1-part extensions
+ assert.are.same({ devicons.get_devicon("foo.lua") }, { "ī ", "#366b8a" })
+ assert.are.same({ devicons.get_devicon("foo.py") }, { "ī", "#805e02" })
+ assert.are.same({ devicons.get_devicon("foo.r") }, { "ó°", "#1a4c8c" })
+ assert.are.same({ devicons.get_devicon("foo.R") }, { "ó°", "#1a4c8c" })
+ end)
+end)
diff --git a/tests/init_spec.lua b/tests/init_spec.lua
index 008c3355..5588ed4d 100644
--- a/tests/init_spec.lua
+++ b/tests/init_spec.lua
@@ -7,7 +7,6 @@ describe("FzfLua", function()
describe("configuration", function()
it("initial setup", function()
fzf.setup({})
- _G.dump(assert.is)
assert.is.truthy(fzf)
end)
end)
diff --git a/tests/libuv_spec.lua b/tests/libuv_spec.lua
new file mode 100644
index 00000000..1ee7d117
--- /dev/null
+++ b/tests/libuv_spec.lua
@@ -0,0 +1,118 @@
+local libuv = require("fzf-lua.libuv")
+
+
+describe("Testing libuv module", function()
+ it("is_escpaed (posix)", function()
+ assert.is.False(libuv.is_escaped([[]], false))
+ assert.is.True(libuv.is_escaped([[""]], false))
+ assert.is.True(libuv.is_escaped([['']], false))
+ assert.is.True(libuv.is_escaped([['foo']], false))
+ end)
+
+ it("is_escpaed (win)", function()
+ assert.is.False(libuv.is_escaped([[]], true))
+ assert.is.True(libuv.is_escaped([[""]], true))
+ assert.is.True(libuv.is_escaped([[^"^"]], true))
+ assert.is.False(libuv.is_escaped([['']], true))
+ end)
+
+ it("shellescape (win bslash)", function()
+ assert.are.same(libuv.shellescape([[]], 1), [[""]])
+ assert.are.same(libuv.shellescape([[^]], 1), [["^"]])
+ assert.are.same(libuv.shellescape([[""]], 1), [["\"\""]])
+ assert.are.same(libuv.shellescape([["^"]], 1), [["\"^\""]])
+ assert.are.same(libuv.shellescape([[foo]], 1), [["foo"]])
+ assert.are.same(libuv.shellescape([["foo"]], 1), [["\"foo\""]])
+ assert.are.same(libuv.shellescape([["foo"bar"]], 1), [["\"foo\"bar\""]])
+ assert.are.same(libuv.shellescape([[foo"bar]], 1), [["foo\"bar"]])
+ assert.are.same(libuv.shellescape([[foo""bar]], 1), [["foo\"\"bar"]])
+ assert.are.same(libuv.shellescape([["foo\"bar"]], 1), [["\"foo\\\"bar\""]])
+ assert.are.same(libuv.shellescape([[foo\]], 1), [["foo\\"]])
+ assert.are.same(libuv.shellescape([[foo\\]], 1), [["foo\\\\"]])
+ assert.are.same(libuv.shellescape([[foo\^]], 1), [["foo\^"]])
+ assert.are.same(libuv.shellescape([[foo\\\\]], 1), [["foo\\\\\\\\"]])
+ assert.are.same(libuv.shellescape([[foo\"]], 1), [["foo\\\""]])
+ assert.are.same(libuv.shellescape([["foo\"]], 1), [["\"foo\\\""]])
+ assert.are.same(libuv.shellescape([["foo\""]], 1), [["\"foo\\\"\""]])
+ assert.are.same(libuv.shellescape([[foo\bar]], 1), [["foo\bar"]])
+ assert.are.same(libuv.shellescape([[foo\\bar]], 1), [["foo\\bar"]])
+ assert.are.same(libuv.shellescape([[foo\\"bar]], 1), [["foo\\\\\"bar"]])
+ assert.are.same(libuv.shellescape([[foo\\\"bar]], 1), [["foo\\\\\\\"bar"]])
+ end)
+
+ it("shellescape (win caret)", function()
+ assert.are.same(libuv.shellescape([[]], 2), [[^"^"]])
+ assert.are.same(libuv.shellescape([["]], 2), [[^"\^"^"]])
+ assert.are.same(libuv.shellescape([[^"]], 2), [[^"^^\^"^"]])
+ assert.are.same(libuv.shellescape([[\"]], 2), [[^"\\\^"^"]])
+ assert.are.same(libuv.shellescape([[\^"]], 2), [[^"^^\\\^"^"]])
+ assert.are.same(libuv.shellescape([[^"^"]], 2), [[^"^^\^"^^\^"^"]])
+ assert.are.same(libuv.shellescape([[__^^"^"__]], 2), [[^"__^^^^\^"^^\^"__^"]])
+ assert.are.same(libuv.shellescape([[__!^^"^"__]], 2),
+ -- 1st: ^"_^!^^^^\^"^^\^"_^"
+ -- 2nd: ^"_^^^!^^^^^^^^^^\\\^"^^^^^^\\\^"_^"
+ [[^"__^^^!^^^^^^^^^^\\\^"^^^^^^\\\^"__^"]])
+ assert.are.same(libuv.shellescape([[__^^^^\^"^^\^"__]], 2),
+ [[^"__^^^^^^^^^^\\\^"^^^^^^\\\^"__^"]])
+ assert.are.same(libuv.shellescape([[^]], 2), [[^"^^^"]])
+ assert.are.same(libuv.shellescape([[^^]], 2), [[^"^^^^^"]])
+ assert.are.same(libuv.shellescape([[^^^]], 2), [[^"^^^^^^^"]])
+ assert.are.same(libuv.shellescape([[^!^]], 2), [[^"^^^^^^^!^^^^^"]])
+ assert.are.same(libuv.shellescape([[!^"]], 2),
+ -- 1st inner: ^!^^\^"
+ -- 2nd inner: ^^^!^^^^^^\\\^"
+ [[^"^^^!^^^^^^\\\^"^"]])
+ assert.are.same(libuv.shellescape([[!\"]], 2), [[^"^^^!^^\\\\\\\^"^"]])
+ assert.are.same(libuv.shellescape([[!\^"]], 2), [[^"^^^!^^^^^^\\\\\\\^"^"]])
+ assert.are.same(libuv.shellescape([[()%^"<>&|]], 2), [[^"^(^)^%^^\^"^<^>^&^|^"]])
+ assert.are.same(libuv.shellescape([[()%^"<>&|!]], 2),
+ -- 1st inner: ^(^)^%^^\^"^<^>^&^|^!
+ -- 2nd inner: ^^^(^^^)^^^%^^^^^^\^"^^^<^^^>^^^&^^^|^^^!
+ [[^"^^^(^^^)^^^%^^^^^^\\\^"^^^<^^^>^^^&^^^|^^^!^"]])
+ assert.are.same(libuv.shellescape([[foo]], 2), [[^"foo^"]])
+ assert.are.same(libuv.shellescape([[foo\]], 2), [[^"foo\\^"]])
+ assert.are.same(libuv.shellescape([[foo^]], 2), [[^"foo^^^"]])
+ assert.are.same(libuv.shellescape([[foo\\]], 2), [[^"foo\\\\^"]])
+ assert.are.same(libuv.shellescape([[foo\\\]], 2), [[^"foo\\\\\\^"]])
+ assert.are.same(libuv.shellescape([[foo\\\\]], 2), [[^"foo\\\\\\\\^"]])
+ assert.are.same(libuv.shellescape([[f!oo]], 2), [[^"f^^^!oo^"]])
+ assert.are.same(libuv.shellescape([[^"foo^"]], 2), [[^"^^\^"foo^^\^"^"]])
+ assert.are.same(libuv.shellescape([[\^"foo\^"]], 2), [[^"^^\\\^"foo^^\\\^"^"]])
+ assert.are.same(libuv.shellescape([[foo""bar]], 2), [[^"foo\^"\^"bar^"]])
+ assert.are.same(libuv.shellescape([[foo^"^"bar]], 2), [[^"foo^^\^"^^\^"bar^"]])
+ assert.are.same(libuv.shellescape([["foo\"bar"]], 2), [[^"\^"foo\\\^"bar\^"^"]])
+ assert.are.same(libuv.shellescape([[foo\^"]], 2), [[^"foo^^\\\^"^"]])
+ assert.are.same(libuv.shellescape([[foo\"]], 2), [[^"foo\\\^"^"]])
+ assert.are.same(libuv.shellescape([[^"foo\^"^"]], 2), [[^"^^\^"foo^^\\\^"^^\^"^"]])
+ assert.are.same(libuv.shellescape([[foo\"bar]], 2), [[^"foo\\\^"bar^"]])
+ assert.are.same(libuv.shellescape([[foo\\"bar]], 2), [[^"foo\\\\\^"bar^"]])
+ assert.are.same(libuv.shellescape([[foo\\^^"bar]], 2), [[^"foo\\^^^^\^"bar^"]])
+ assert.are.same(libuv.shellescape([[foo\\\^^^"]], 2), [[^"foo\\\^^^^^^\^"^"]])
+ end)
+
+ it("escape {q} (win)", function()
+ assert.are.same(libuv.escape_fzf([[]], true), [[]])
+ assert.are.same(libuv.escape_fzf([[\]], true), [[\]])
+ assert.are.same(libuv.escape_fzf([[\\]], true), [[\\]])
+ assert.are.same(libuv.escape_fzf([[foo]], true), [[foo]])
+ assert.are.same(libuv.escape_fzf([[\foo]], true), [[\\foo]])
+ assert.are.same(libuv.escape_fzf([[\\foo]], true), [[\\\\foo]])
+ assert.are.same(libuv.escape_fzf([[\\\foo]], true), [[\\\\\\foo]])
+ assert.are.same(libuv.escape_fzf([[\\\\foo]], true), [[\\\\\\\\foo]])
+ assert.are.same(libuv.escape_fzf([[foo\]], true), [[foo\]])
+ assert.are.same(libuv.escape_fzf([[foo\\]], true), [[foo\\]])
+ end)
+
+ it("unescape {q} (win)", function()
+ assert.are.same(libuv.unescape_fzf([[]], true), [[]])
+ assert.are.same(libuv.unescape_fzf([[\]], true), [[\]])
+ assert.are.same(libuv.unescape_fzf([[\\]], true), [[\\]])
+ assert.are.same(libuv.unescape_fzf([[foo]], true), [[foo]])
+ assert.are.same(libuv.unescape_fzf([[\foo]], true), [[\foo]])
+ assert.are.same(libuv.unescape_fzf([[\\foo]], true), [[\foo]])
+ assert.are.same(libuv.unescape_fzf([[\\\foo]], true), [[\foo]])
+ assert.are.same(libuv.unescape_fzf([[\\\\foo]], true), [[\\foo]])
+ assert.are.same(libuv.unescape_fzf([[foo\]], true), [[foo\]])
+ assert.are.same(libuv.unescape_fzf([[foo\\]], true), [[foo\\]])
+ end)
+end)
diff --git a/tests/minimal_init.lua b/tests/minimal_init.lua
new file mode 100644
index 00000000..e0286fe5
--- /dev/null
+++ b/tests/minimal_init.lua
@@ -0,0 +1,6 @@
+vim.opt.hidden = true
+vim.opt.swapfile = false
+
+vim.opt.runtimepath:append("../plenary.nvim")
+vim.opt.runtimepath:append("../fzf-lua")
+vim.cmd("runtime! plugin/plenary.vim")
diff --git a/tests/minimal_init.vim b/tests/minimal_init.vim
deleted file mode 100644
index d2e11884..00000000
--- a/tests/minimal_init.vim
+++ /dev/null
@@ -1,6 +0,0 @@
-set hidden
-set noswapfile
-
-set rtp+=../plenary.nvim
-set rtp+=../fzf-lua
-runtime! plugin/plenary.vim
diff --git a/tests/path_spec.lua b/tests/path_spec.lua
new file mode 100644
index 00000000..08cce0ef
--- /dev/null
+++ b/tests/path_spec.lua
@@ -0,0 +1,344 @@
+local fzf = require("fzf-lua")
+local path = fzf.path
+local utils = fzf.utils
+
+describe("Testing path module", function()
+ it("separator", function()
+ utils.__IS_WINDOWS = false
+ assert.are.same(path.separator(), "/")
+ assert.are.same(path.separator(""), "/")
+ assert.are.same(path.separator("~/foo"), "/")
+ assert.are.same(path.separator([[~\foo]]), "/")
+ assert.are.same(path.separator([[c:\foo]]), "/")
+
+ utils.__IS_WINDOWS = true
+ assert.are.same(path.separator(), [[\]])
+ assert.are.same(path.separator(""), [[\]])
+ assert.are.same(path.separator("~/foo"), "/")
+ assert.are.same(path.separator([[~\foo]]), [[\]])
+ assert.are.same(path.separator([[c:\foo]]), [[\]])
+ assert.are.same(path.separator([[foo\bar]]), [[\]])
+ end)
+
+ it("Ends with separator", function()
+ utils.__IS_WINDOWS = false
+ assert.is.False(path.ends_with_separator(""))
+ assert.is.True(path.ends_with_separator("/"))
+ assert.is.False(path.ends_with_separator([[\]]))
+ assert.is.False(path.ends_with_separator("/some/path"))
+ assert.is.True(path.ends_with_separator("/some/path/"))
+
+ utils.__IS_WINDOWS = true
+ assert.is.False(path.ends_with_separator(""))
+ assert.is.True(path.ends_with_separator("/"))
+ assert.is.True(path.ends_with_separator([[\]]))
+ assert.is.False(path.ends_with_separator("/some/path"))
+ assert.is.True(path.ends_with_separator("/some/path/"))
+ assert.is.False(path.ends_with_separator([[c:\some\path]]))
+ assert.is.True(path.ends_with_separator([[c:\some\path\]]))
+ end)
+
+ it("Add trailing separator", function()
+ utils.__IS_WINDOWS = false
+ assert.are.equal(path.add_trailing(""), "/")
+ assert.are.equal(path.add_trailing("/"), "/")
+ assert.are.equal(path.add_trailing("/some"), "/some/")
+ assert.are.equal(path.add_trailing("/some/"), "/some/")
+ utils.__IS_WINDOWS = true
+ assert.are.equal(path.add_trailing(""), [[\]])
+ assert.are.equal(path.add_trailing("/"), [[/]])
+ assert.are.equal(path.add_trailing("/some"), [[/some\]])
+ assert.are.equal(path.add_trailing("/some/"), [[/some/]])
+ assert.are.equal(path.add_trailing([[C:\some\]]), [[C:\some\]])
+ assert.are.equal(path.add_trailing([[C:\some]]), [[C:\some\]])
+ assert.are.equal(path.add_trailing([[C:/some]]), [[C:/some/]])
+ assert.are.equal(path.add_trailing([[~/some]]), [[~/some/]])
+ assert.are.equal(path.add_trailing([[some\path]]), [[some\path\]])
+ end)
+
+ it("Remove trailing separator", function()
+ utils.__IS_WINDOWS = false
+ assert.are.equal(path.remove_trailing(""), "")
+ assert.are.equal(path.remove_trailing("/"), "")
+ assert.are.equal(path.remove_trailing("//"), "")
+ assert.are.equal(path.remove_trailing("/some"), "/some")
+ assert.are.equal(path.remove_trailing("/some/"), "/some")
+ assert.are.equal(path.remove_trailing("/some/////"), "/some")
+ assert.are.equal(path.remove_trailing([[/some\]]), [[/some\]])
+ utils.__IS_WINDOWS = true
+ assert.are.equal(path.remove_trailing(""), "")
+ assert.are.equal(path.remove_trailing("/"), "")
+ assert.are.equal(path.remove_trailing("//"), "")
+ assert.are.equal(path.remove_trailing("/some"), "/some")
+ assert.are.equal(path.remove_trailing("/some/"), "/some")
+ assert.are.equal(path.remove_trailing("/some/////"), "/some")
+ assert.are.equal(path.remove_trailing([[/some\]]), [[/some]])
+ assert.are.equal(path.remove_trailing([[C:\some\]]), [[C:\some]])
+ assert.are.equal(path.remove_trailing([[C:\some\\\\//]]), [[C:\some]])
+ end)
+
+ it("Is absolute", function()
+ utils.__IS_WINDOWS = false
+ assert.is.False(path.is_absolute(""))
+ assert.is.True(path.is_absolute("/"))
+ assert.is.False(path.is_absolute([[\]]))
+ assert.is.True(path.is_absolute("/some/path"))
+ assert.is.False(path.is_absolute("./some/path/"))
+ assert.is.False(path.is_absolute([[c:\some\path\]]))
+
+ utils.__IS_WINDOWS = true
+ assert.is.False(path.is_absolute(""))
+ assert.is.False(path.is_absolute("/"))
+ assert.is.False(path.is_absolute([[\]]))
+ assert.is.False(path.is_absolute("/some/path"))
+ assert.is.False(path.is_absolute("./some/path/"))
+ assert.is.False(path.is_absolute([[.\some\path/]]))
+ assert.is.True(path.is_absolute([[c:\some\path]]))
+ assert.is.True(path.is_absolute([[C:\some\path\]]))
+ end)
+
+ it("Has cwd prefix", function()
+ utils.__IS_WINDOWS = false
+ assert.is.False(path.has_cwd_prefix(""))
+ assert.is.True(path.has_cwd_prefix("./"))
+ assert.is.False(path.has_cwd_prefix([[.\]]))
+ assert.is.False(path.has_cwd_prefix("/some/path"))
+ assert.is.True(path.has_cwd_prefix("./some/path/"))
+
+ utils.__IS_WINDOWS = true
+ assert.is.False(path.has_cwd_prefix(""))
+ assert.is.True(path.has_cwd_prefix("./"))
+ assert.is.True(path.has_cwd_prefix([[.\]]))
+ assert.is.False(path.has_cwd_prefix("/some/path"))
+ assert.is.True(path.has_cwd_prefix("./some/path/"))
+ assert.is.True(path.has_cwd_prefix([[.\some\path/]]))
+ assert.is.False(path.has_cwd_prefix([[c:\some\path]]))
+ assert.is.False(path.has_cwd_prefix([[c:\some\path\]]))
+ assert.is.False(path.has_cwd_prefix([[c:\some/path\]]))
+ end)
+
+ it("Strip cwd prefix", function()
+ utils.__IS_WINDOWS = false
+ assert.are.equal(path.strip_cwd_prefix(""), "")
+ assert.are.equal(path.strip_cwd_prefix("./"), "")
+ assert.are.equal(path.strip_cwd_prefix([[.\]]), [[.\]])
+ assert.are.equal(path.strip_cwd_prefix("/some/path"), "/some/path")
+ assert.are.equal(path.strip_cwd_prefix("./some/path/"), "some/path/")
+
+ utils.__IS_WINDOWS = true
+ assert.are.equal(path.strip_cwd_prefix(""), "")
+ assert.are.equal(path.strip_cwd_prefix("./"), "")
+ assert.are.equal(path.strip_cwd_prefix([[.\]]), "")
+ assert.are.equal(path.strip_cwd_prefix("/some/path"), "/some/path")
+ assert.are.equal(path.strip_cwd_prefix("./some/path/"), "some/path/")
+ assert.are.equal(path.strip_cwd_prefix([[.\some\path/]]), [[some\path/]])
+ assert.are.equal(path.strip_cwd_prefix([[c:\some\path]]), [[c:\some\path]])
+ end)
+
+ it("Tail", function()
+ utils.__IS_WINDOWS = false
+ assert.are.equal(path.tail(""), "")
+ assert.are.equal(path.tail("/"), "/")
+ assert.are.equal(path.tail("foo"), "foo")
+ assert.are.equal(path.tail(".foo"), ".foo")
+ assert.are.equal(path.tail("/foo"), "foo")
+ assert.are.equal(path.tail("/foo/"), "foo/")
+ assert.are.equal(path.tail("/foo/bar"), "bar")
+ assert.are.equal(path.tail([[/foo\bar]]), [[foo\bar]])
+
+ utils.__IS_WINDOWS = true
+ assert.are.equal(path.tail(""), "")
+ assert.are.equal(path.tail("/"), "/")
+ assert.are.equal(path.tail([[\]]), [[\]])
+ assert.are.equal(path.tail("foo"), "foo")
+ assert.are.equal(path.tail(".foo"), ".foo")
+ assert.are.equal(path.tail([[c:\foo]]), "foo")
+ assert.are.equal(path.tail([[c:\foo\]]), [[foo\]])
+ assert.are.equal(path.tail([[c:\foo\bar]]), "bar")
+ assert.are.equal(path.tail([[c:/foo\bar]]), "bar")
+ assert.are.equal(path.tail([[c:/foo//\//bar]]), "bar")
+ end)
+
+ it("Parent", function()
+ utils.__IS_WINDOWS = false
+ assert.are.equal(path.parent(""), nil)
+ assert.are.equal(path.parent("/"), nil)
+ assert.are.equal(path.parent("/foo"), "/")
+ assert.are.equal(path.parent("/foo/bar"), "/foo/")
+ assert.are.equal(path.parent("/foo/bar", true), "/foo")
+ assert.are.equal(path.parent([[/foo\bar]]), [[/]])
+
+ utils.__IS_WINDOWS = true
+ assert.are.equal(path.parent(""), nil)
+ assert.are.equal(path.parent("/"), nil)
+ assert.are.equal(path.parent([[\]]), nil)
+ assert.are.equal(path.parent([[c:]]), nil)
+ assert.are.equal(path.parent([[c:\foo]]), [[c:\]])
+ assert.are.equal(path.parent([[c:\foo]], true), [[c:]])
+ assert.are.equal(path.parent([[c:\foo\bar]]), [[c:\foo\]])
+ assert.are.equal(path.parent([[c:\foo/bar]]), [[c:\foo/]])
+ assert.are.equal(path.parent([[c:/foo//\//bar]], true), [[c:/foo]])
+ end)
+
+ it("Normalize", function()
+ utils.__IS_WINDOWS = false
+ assert.are.equal(path.normalize("/some/path"), "/some/path")
+ assert.are.equal(path.normalize([[\some\path]]), [[\some\path]])
+ assert.are.equal(path.normalize("~/some/path"), path.HOME() .. "/some/path")
+ utils.__IS_WINDOWS = true
+ assert.are.equal(path.normalize("/some/path"), "/some/path")
+ assert.are.equal(path.normalize([[\some\path]]), "/some/path")
+ assert.are.equal(path.normalize("~/some/path"), path.normalize(path.HOME()) .. "/some/path")
+ end)
+
+ it("Equals", function()
+ utils.__IS_WINDOWS = false
+ assert.is.False(path.equals("/some/path", "/some/path/foo"))
+ assert.is.True(path.equals("/some/path", "/some/path/"))
+ assert.is.False(path.equals("/some/Path", "/some/path"))
+ assert.is.True(path.equals("~/some/path", path.HOME() .. "/some/path"))
+ assert.is.False(path.equals([[/some\\path]], "/some/path/"))
+ utils.__IS_WINDOWS = true
+ assert.is.False(path.equals("/some/path", "/some/path/foo"))
+ assert.is.True(path.equals("/some/path", "/some/path/"))
+ assert.is.True(path.equals("/some/PATH", "/some/path"))
+ assert.is.True(path.equals("~/some/path", path.HOME() .. "/some/path"))
+ assert.is.True(path.equals([[/some\path\\]], "/some/path/"))
+ end)
+
+ it("Is relative to", function()
+ -- Testing both `path.is_relative_to` and `path.relative_to`
+ -- [1]: path
+ -- [2]: relative_to
+ -- [3]: expected result (bool, relative_path)
+ local unix = {
+ { "/some/path", "/some/path", { true, "." } },
+ { "/some/path", "/some/path//", { true, "." } },
+ { "/some/path//", "/some/path", { true, "." } },
+ { "/some", "/somepath", { false, nil } },
+ { "some", "somepath", { false, nil } },
+ { "some", "some/path", { false, nil } },
+ { "some/path", "some", { true, "path" } },
+ { "some/path/", "some", { true, "path/" } },
+ { "some/path//", "some", { true, "path//" } },
+ { "some/path/", [[some\]], { false, nil } },
+ { [[some\path]], "some", { false, nil } },
+ { "/some/path/to", "/some///", { true, "path/to" } },
+ { "/SOME/PATH", "/some", { false, nil } },
+ { "a///b//////c", "a//b", { true, "c" } },
+ { "~", path.HOME(), { true, "." } },
+ { "~/a/b/c", "~/a", { true, "b/c" } },
+ { "~//a/b/c", path.HOME() .. "/a/b", { true, "c" } },
+ { path.HOME() .. "/a/b", "~/a/", { true, "b" } },
+ { "/", "/some/path", { false, nil } },
+ { "/some", "/some/path", { false, nil } },
+ }
+ local win = {
+ { [[\some\path]], [[\some/path]], { true, "." } },
+ { [[/some/path]], [[/some/path\/\/]], { true, "." } },
+ { "/some/path/", "/some/path", { true, "." } },
+ { [[\some]], [[\somepath]], { false, nil } },
+ { "some/path", "some", { true, "path" } },
+ { [[some\path\]], [[some\/\]], { true, [[path\]] } },
+ { [[c:\some\path]], [[c:\some]], { true, [[path]] } },
+ { [[c:\some\path\]], [[c:\some]], { true, [[path\]] } },
+ { [[c:\some\path\\]], [[c:\some]], { true, [[path\\]] } },
+ { [[c:\some\path\]], [[c:\some\\///]], { true, [[path\]] } },
+ { [[c:\SOME\PATH]], [[C:\some/\/\]], { true, [[PATH]] } },
+ { [[~\SOME\PATH\to]], [[~\some/]], { true, [[PATH\to]] } },
+ { [[~\SOME\PATH/to]], [[~\some]], { true, [[PATH/to]] } },
+ { [[C:\a/\/b///\\\c]], [[c:\/\a\/b]], { true, "c" } },
+ { [[C:\a/b\c\d\e]], [[c:\A\/b\]], { true, [[c\d\e]] } },
+ { [[C:\Users]], [[c:\]], { true, [[Users]] } },
+ { [[C:\Users\test]], [[c:\]], { true, [[Users\test]] } },
+ { [[C:\]], [[C:\Users]], { false, nil } },
+ { [[C:]], [[C:\Users]], { false, nil } },
+ }
+ utils.__IS_WINDOWS = false
+ for _, v in ipairs(unix) do
+ assert.are.same({ path.is_relative_to(v[1], v[2]) }, v[3],
+ string.format('\n\nis_relative_to("%s", "%s") ~= "%s"\n', v[1], v[2], v[3][2]))
+ end
+ utils.__IS_WINDOWS = true
+ for _, v in ipairs(win) do
+ assert.are.same({ path.is_relative_to(v[1], v[2]) }, v[3],
+ string.format('\n\nis_relative_to("%s", "%s") ~= "%s"\n', v[1], v[2], v[3][2]))
+ end
+ end)
+
+ it("Extension", function()
+ utils.__IS_WINDOWS = false
+ assert.are.equal(path.extension("/some/path/foobar"), nil)
+ assert.are.equal(path.extension("/some/pa.th/foobar"), nil)
+ assert.is.same(path.extension("/some/path/foobar."), "")
+ assert.is.same(path.extension("/some/path/foo.bar"), "bar")
+ assert.is.same(path.extension("/some/path/foo.bar.baz"), "baz")
+ assert.is.same(path.extension("/some/path/.foobar"), "foobar")
+ -- override that doesn't "tail" the path
+ assert.are.equal(path.extension("/some/pa.th/foobar", true), "th/foobar")
+ end)
+
+ it("Join", function()
+ utils.__IS_WINDOWS = false
+ assert.is.same(path.join({ "some" }), "some")
+ assert.is.same(path.join({ nil, "path" }), "path")
+ assert.is.same(path.join({ "/some", "path" }), "/some/path")
+ assert.is.same(path.join({ "/some", "path/" }), "/some/path/")
+ assert.is.same(path.join({ "/some/", "path" }), "/some/path")
+ assert.is.same(path.join({ "/some//", "path" }), "/some//path")
+ assert.is.same(path.join({ "~/some/", "path" }), "~/some/path")
+ assert.is.same(path.join({ [[~\some\]], "path" }), [[~\some\/path]])
+ assert.is.same(path.join({ [[c:\some\]], "path" }), [[c:\some\/path]])
+ utils.__IS_WINDOWS = true
+ assert.is.same(path.join({ "some" }), "some")
+ assert.is.same(path.join({ nil, "path" }), "path")
+ assert.is.same(path.join({ "some", "path" }), [[some\path]])
+ assert.is.same(path.join({ "some", [[path\]] }), [[some\path\]])
+ assert.is.same(path.join({ "c:/some", "path" }), "c:/some/path")
+ assert.is.same(path.join({ [[c:\some\]], "path" }), [[c:\some\path]])
+ assert.is.same(path.join({ [[c:\some\]], "path", "foo", "bar" }), [[c:\some\path\foo\bar]])
+ assert.is.same(path.join({ "~/some/", "path" }), "~/some/path")
+ assert.is.same(path.join({ [[~\some\]], "path" }), [[~\some\path]])
+ end)
+
+ it("Shorten", function()
+ utils.__IS_WINDOWS = false
+ assert.are.equal(path.shorten(""), "")
+ assert.are.equal(path.shorten("/"), "/")
+ assert.are.equal(path.shorten("/foo"), "/foo")
+ assert.are.equal(path.shorten("/foo/"), "/f/")
+ assert.are.equal(path.shorten("/foo/bar"), "/f/bar")
+ assert.are.equal(path.shorten("/foo/bar/baz"), "/f/b/baz")
+ assert.are.equal(path.shorten("~/foo/bar/baz"), "~/f/b/baz")
+ assert.are.equal(path.shorten("~/foo/bar/baz/"), "~/f/b/b/")
+ assert.are.equal(path.shorten("~/foo/bar/baz//"), "~/f/b/b//")
+ assert.are.equal(path.shorten("~/.foo/.bar/baz"), "~/.f/.b/baz")
+ assert.are.equal(path.shorten("~/foo/bar/baz", 2), "~/fo/ba/baz")
+ assert.are.equal(path.shorten("~/fo/barrr/baz", 3), "~/fo/bar/baz")
+ assert.are.equal(path.shorten([[/foo\bar]]), [[/foo\bar]])
+
+ utils.__IS_WINDOWS = true
+ assert.are.equal(path.shorten("/foo"), [[\foo]])
+ assert.are.equal(path.shorten([[/foo\bar]]), [[\f\bar]])
+ assert.are.equal(path.shorten([[/foo/bar]]), [[\f\bar]])
+ assert.are.equal(path.shorten([[/foo/bar\baz]]), [[\f\b\baz]])
+ assert.are.equal(path.shorten([[\]]), [[\]])
+ assert.are.equal(path.shorten([[c:/]]), [[c:/]])
+ assert.are.equal(path.shorten([[c:\]]), [[c:\]])
+ assert.are.equal(path.shorten([[c:\foo]]), [[c:\foo]])
+ assert.are.equal(path.shorten([[c:\foo\bar]]), [[c:\f\bar]])
+ assert.are.equal(path.shorten([[c:\foo\bar\baz]]), [[c:\f\b\baz]])
+ assert.are.equal(path.shorten([[c:\.foo\.bar\baz]]), [[c:\.f\.b\baz]])
+ assert.are.equal(path.shorten([[c:/foo\bar]]), [[c:/f/bar]])
+ assert.are.equal(path.shorten([[~/foo/bar]]), [[~/f/bar]])
+ assert.are.equal(path.shorten([[~\foo\bar]]), [[~\f\bar]])
+ assert.are.equal(path.shorten([[~\foo/bar]]), [[~\f\bar]])
+ assert.are.equal(path.shorten([[~\foo\bar\]]), [[~\f\b\]])
+ -- override separator auto-detect
+ assert.are.equal(path.shorten([[c:\foo\bar]], nil, [[/]]), [[c:/f/bar]])
+ assert.are.equal(path.shorten([[c:/foo\bar]], nil, [[\]]), [[c:\f\bar]])
+ -- shorten len
+ assert.are.equal(path.shorten([[c:\foo\bar\baz]], 2), [[c:\fo\ba\baz]])
+ assert.are.equal(path.shorten([[c:/foo\bar\baz]], 2), [[c:/fo/ba/baz]])
+ end)
+end)
diff --git a/tests/utils_spec.lua b/tests/utils_spec.lua
new file mode 100644
index 00000000..195bfeb4
--- /dev/null
+++ b/tests/utils_spec.lua
@@ -0,0 +1,17 @@
+local fzf = require("fzf-lua")
+local utils = fzf.utils
+
+describe("Testing utils module", function()
+ it("separator", function()
+ utils.__IS_WINDOWS = false
+ assert.are.same(utils._if_win_normalize_vars("--w=$COLUMNS"), "--w=$COLUMNS")
+
+ utils.__IS_WINDOWS = true
+ assert.are.same(utils._if_win_normalize_vars("--w=$COLUMNS", 1), "--w=%COLUMNS%")
+ assert.are.same(utils._if_win_normalize_vars("--w=%COLUMNS%", 1), "--w=%COLUMNS%")
+ assert.are.same(utils._if_win_normalize_vars("-w=$C -l=$L", 1), "-w=%C% -l=%L%")
+ assert.are.same(utils._if_win_normalize_vars("--w=$COLUMNS", 2), "--w=!COLUMNS!")
+ assert.are.same(utils._if_win_normalize_vars("--w=%COLUMNS%", 2), "--w=!COLUMNS!")
+ assert.are.same(utils._if_win_normalize_vars("-w=$C -l=$L", 2), "-w=!C! -l=!L!")
+ end)
+end)