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, "") 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)