From 930cc5f4875c24749be883d81ec33158e1f3e52a Mon Sep 17 00:00:00 2001 From: bhagwan Date: Fri, 11 Feb 2022 17:08:34 -0800 Subject: [PATCH] added support for nvim-dap (#101) --- README.md | 10 +- doc/fzf-lua.txt | 14 +- lua/fzf-lua/actions.lua | 14 +- lua/fzf-lua/config.lua | 29 ++- lua/fzf-lua/init.lua | 6 + lua/fzf-lua/previewer/fzf.lua | 4 +- lua/fzf-lua/providers/buffers.lua | 2 + lua/fzf-lua/providers/dap.lua | 265 ++++++++++++++++++++++++++++ lua/fzf-lua/providers/lsp.lua | 37 ++-- lua/fzf-lua/providers/ui_select.lua | 6 +- 10 files changed, 366 insertions(+), 21 deletions(-) create mode 100644 lua/fzf-lua/providers/dap.lua diff --git a/README.md b/README.md index b2eb732..55aee13 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,14 @@ vim.api.nvim_set_keymap('n', '', | `filetypes` | neovim filetypes | | `packadd` | :packadd | +### nvim-dap +| Command | List | +| -------------------- | ------------------------------------------ | +| `dap_commands` | list,run `nvim-dap` builtin commands | +| `dap_configurations` | list,run debug configurations | +| `dap_breakpoints` | list,delete breakpoints | +| `dap_variables` | active session variables | +| `dap_frames` | active session jump to frame | ### Neovim API @@ -573,7 +581,7 @@ require'fzf-lua'.setup { fzf_opts = { -- do not include bufnr in fuzzy matching -- tiebreak by line no. - ['--delimiter'] = vim.fn.shellescape(']'), + ['--delimiter'] = vim.fn.shellescape('[\\]]'), ["--nth"] = '2..', ["--tiebreak"] = 'index', }, diff --git a/doc/fzf-lua.txt b/doc/fzf-lua.txt index c591f11..2bdf65b 100644 --- a/doc/fzf-lua.txt +++ b/doc/fzf-lua.txt @@ -236,6 +236,18 @@ MISC *fzf-lua-misc* +NVIM-DAP *fzf-lua-nvim-dap* + +| Command | List | +| -------------------- | ------------------------------------------ | +| `dap_commands` | list,run `nvim-dap` builtin commands | +| `dap_configurations` | list,run debug configurations | +| `dap_breakpoints` | list,delete breakpoints | +| `dap_variables` | active session variables | +| `dap_frames` | active session jump to frame | + + + NEOVIM API *fzf-lua-neovim-api* `:help vim.ui.select` for more info @@ -612,7 +624,7 @@ Consult the list below for available settings: fzf_opts = { -- do not include bufnr in fuzzy matching -- tiebreak by line no. - ['--delimiter'] = vim.fn.shellescape(']'), + ['--delimiter'] = vim.fn.shellescape('[\\]]'), ["--nth"] = '2..', ["--tiebreak"] = 'index', }, diff --git a/lua/fzf-lua/actions.lua b/lua/fzf-lua/actions.lua index 5b0b634..4402295 100644 --- a/lua/fzf-lua/actions.lua +++ b/lua/fzf-lua/actions.lua @@ -118,7 +118,9 @@ M.vimcmd_file = function(vimcmd, selected, opts) vim.api.nvim_win_set_cursor(0, {1, 0}) vim.fn.search(entry.ctag, "W") elseif entry.line>1 or entry.col>1 then - entry.col = entry.col or 1 + -- make sure we have valid column + -- 'nvim-dap' for example sets columns to 0 + entry.col = entry.col and entry.col>0 and entry.col or 1 vim.api.nvim_win_set_cursor(0, {tonumber(entry.line), tonumber(entry.col)-1}) end if not is_term then vim.cmd("norm! zvzz") end @@ -284,9 +286,7 @@ M.colorscheme = function(selected) vim.cmd("colorscheme " .. colorscheme) end -M.run_builtin = function(selected) - local method = selected[1] - vim.cmd(string.format("lua require'fzf-lua'.%s()", method)) +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 @@ -311,6 +311,12 @@ M.run_builtin = function(selected) end end +M.run_builtin = function(selected) + local method = selected[1] + vim.cmd(string.format("lua require'fzf-lua'.%s()", method)) + M.ensure_insert_mode() +end + M.ex_run = function(selected) local cmd = selected[1] vim.cmd("stopinsert") diff --git a/lua/fzf-lua/config.lua b/lua/fzf-lua/config.lua index 85930c9..cc6abd4 100644 --- a/lua/fzf-lua/config.lua +++ b/lua/fzf-lua/config.lua @@ -343,7 +343,7 @@ M.globals.lines = { show_unlisted = false, no_term_buffers = true, fzf_opts = { - ['--delimiter'] = vim.fn.shellescape(']'), + ['--delimiter'] = vim.fn.shellescape('[\\]]'), ["--nth"] = '2..', ["--tiebreak"] = 'index', }, @@ -519,6 +519,33 @@ M.globals.nvim = { }, } +M.globals.dap = { + commands = { + prompt = 'DAP Commands> ', + }, + configurations = { + prompt = 'DAP Configurations> ', + }, + variables = { + prompt = 'DAP Variables> ', + }, + frames = { + prompt = 'DAP Frames> ', + }, + breakpoints = { + prompt = 'DAP Breakpoints> ', + file_icons = true and M._has_devicons, + color_icons = true, + git_icons = true, + previewer = M._default_previewer_fn, + _actions = function() return M.globals.actions.files end, + fzf_opts = { + ['--delimiter'] = vim.fn.shellescape('[\\]]'), + ["--with-nth"] = '2..', + }, + }, + } + M.globals.file_icon_padding = '' if not M._has_devicons then diff --git a/lua/fzf-lua/init.lua b/lua/fzf-lua/init.lua index b36ed5e..3634579 100644 --- a/lua/fzf-lua/init.lua +++ b/lua/fzf-lua/init.lua @@ -151,6 +151,12 @@ M.lsp_workspace_diagnostics = require'fzf-lua.providers.lsp'.workspace_diagnosti M.register_ui_select = require'fzf-lua.providers.ui_select'.register M.deregister_ui_select = require'fzf-lua.providers.ui_select'.deregister +M.dap_commands = require'fzf-lua.providers.dap'.commands +M.dap_configurations = require'fzf-lua.providers.dap'.configurations +M.dap_breakpoints = require'fzf-lua.providers.dap'.breakpoints +M.dap_variables = require'fzf-lua.providers.dap'.variables +M.dap_frames = require'fzf-lua.providers.dap'.frames + -- API shortcuts M.fzf = require'fzf-lua.core'.fzf M.fzf_wrap = require'fzf-lua.core'.fzf_wrap diff --git a/lua/fzf-lua/previewer/fzf.lua b/lua/fzf-lua/previewer/fzf.lua index c820b78..3b71880 100644 --- a/lua/fzf-lua/previewer/fzf.lua +++ b/lua/fzf-lua/previewer/fzf.lua @@ -48,9 +48,9 @@ function Previewer.base:fzf_delimiter() delim = '[:]' elseif not delim:match(":") then if delim:match("%[.*%]")then - delim = '[:' .. delim:match("%[(.*%])") + delim = delim:match("(%[.*)%]") .. ':]' else - delim = '[:' .. delim .. ']' + delim = '[' .. delim .. ':]' end end return delim diff --git a/lua/fzf-lua/providers/buffers.lua b/lua/fzf-lua/providers/buffers.lua index 067199b..2f92374 100644 --- a/lua/fzf-lua/providers/buffers.lua +++ b/lua/fzf-lua/providers/buffers.lua @@ -260,6 +260,8 @@ M.buffer_lines = function(opts) opts.fzf_opts['--query'] = vim.fn.shellescape(opts.search) end + opts = core.set_fzf_line_args(opts) + core.fzf_wrap(opts, items, function(selected) if not selected then return end diff --git a/lua/fzf-lua/providers/dap.lua b/lua/fzf-lua/providers/dap.lua new file mode 100644 index 0000000..1943ee4 --- /dev/null +++ b/lua/fzf-lua/providers/dap.lua @@ -0,0 +1,265 @@ +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 actions = require "fzf-lua.actions" + +local _has_dap, _dap = nil, nil + +local M = {} + +-- attempt to load 'nvim-dap' every call +-- in case the plugin was lazy loaded +local function dap() + if _has_dap and _dap then return _dap end + _has_dap, _dap = pcall(require, 'dap') + if not _has_dap or not _dap then + utils.info("DAP requires 'mfussenegger/nvim-dap'") + return false + end + return true +end + +M.commands = function(opts) + if not dap() then return end + + opts = config.normalize_opts(opts, config.globals.dap.commands) + if not opts then return end + + local entries = {} + for k, v in pairs(_dap) do + if type(v) == "function" then + table.insert(entries, k) + end + end + + opts.actions = { + ["default"] = opts.actions and opts.actions.default or + function(selected, _) + _dap[selected[1]]() + if require'fzf-lua.providers.ui_select'.is_registered() then + -- opening an fzf-lua win from another requires this + actions.ensure_insert_mode() + end + end, + } + + opts.fzf_opts['--no-multi'] = '' + + core.fzf_wrap(opts, entries, function(selected) + + if not selected then return end + actions.act(opts.actions, selected) + + end)() +end + +M.configurations = function(opts) + if not dap() then return end + + opts = config.normalize_opts(opts, config.globals.dap.configurations) + if not opts then return end + + local entries = {} + opts._cfgs = {} + for lang, lang_cfgs in pairs(_dap.configurations) do + for _, cfg in ipairs(lang_cfgs) do + opts._cfgs[#entries+1] = cfg + table.insert(entries, ("[%s] %s. %s"):format( + utils.ansi_codes.green(lang), + utils.ansi_codes.magenta(tostring(#entries+1)), + cfg.name + )) + end + end + + opts.actions = { + ["default"] = opts.actions and opts.actions.default or + function(selected, _) + -- cannot run while in session + if _dap.session() then return end + local idx = selected and tonumber(selected[1]:match("(%d+).")) or nil + if idx and opts._cfgs[idx] then + _dap.run(opts._cfgs[idx]) + end + end, + } + + opts.fzf_opts['--no-multi'] = '' + + core.fzf_wrap(opts, entries, function(selected) + + if not selected then return end + actions.act(opts.actions, selected) + + end)() +end + +M.breakpoints = function(opts) + if not dap() then return end + local dap_bps = require'dap.breakpoints' + + opts = config.normalize_opts(opts, config.globals.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 vim.tbl_isempty(opts._locations) 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.cwd) + 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, core.make_entry_lcol(opts, entry)) + 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)), + core.make_entry_file(opts, x)) + 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, function() end) + end) + end + end + cb(nil, function() end) + 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(""))) + end + + opts = core.set_fzf_line_args(opts) + + core.fzf_wrap(opts, contents, function(selected) + + if not selected then return end + actions.act(opts.actions, selected, opts) + + end)() + +end + +M.variables = function(opts) + if not dap() then return end + + opts = config.normalize_opts(opts, config.globals.dap.variables) + if not opts then return end + + local session = _dap.session() + if not session then + utils.info("No active DAP session.") + return + end + + local entries = {} + for _, s in pairs(session.current_frame.scopes or {}) do + if s.variables then + for _, v in pairs(s.variables) do + if v.type ~= '' and v.value ~= '' then + table.insert(entries, ("[%s] %s = %s"):format( + utils.ansi_codes.green(v.type), + -- utils.ansi_codes.red(v.name), + v.name, + v.value + )) + end + end + end + end + + core.fzf_wrap(opts, entries, function(selected) + + if not selected then return end + actions.act(opts.actions, selected) + + end)() + +end + +M.frames = function(opts) + if not dap() then return end + + opts = config.normalize_opts(opts, config.globals.dap.frames) + if not opts then return end + + local session = _dap.session() + if not session then + utils.info("No active DAP session.") + return + end + + if not session.stopped_thread_id then + utils.info("Unable to switch frames unless stopped.") + return + end + + opts._frames = session.threads[session.stopped_thread_id].frames + + opts.actions = { + ["default"] = opts.actions and opts.actions.default or + function(selected, o) + local sess = _dap.session() + if not sess or not sess.stopped_thread_id then return end + local idx = selected and tonumber(selected[1]:match("(%d+).")) or nil + if idx and o._frames[idx] then + session:_frame_set(o._frames[idx]) + end + end, + } + + local entries = {} + for i, f in ipairs(opts._frames) do + table.insert(entries, ("%s. [%s] %s%s"):format( + utils.ansi_codes.magenta(tostring(i)), + utils.ansi_codes.green(f.name), + f.source and f.source.name or '' , + f.line and ((":%d"):format(f.line)) or '' + )) + end + + opts.fzf_opts['--no-multi'] = '' + + core.fzf_wrap(opts, entries, function(selected) + + if not selected then return end + actions.act(opts.actions, selected, opts) + + end)() + +end + +return M diff --git a/lua/fzf-lua/providers/lsp.lua b/lua/fzf-lua/providers/lsp.lua index 1926957..8c94e11 100644 --- a/lua/fzf-lua/providers/lsp.lua +++ b/lua/fzf-lua/providers/lsp.lua @@ -345,6 +345,29 @@ end M.code_actions = function(opts) opts = normalize_lsp_opts(opts, config.globals.lsp) if not opts then return end + + -- irrelevant for code actions and can cause + -- single results to be skipped with 'async = false' + opts.jump_to_single_result = false + opts.lsp_params = vim.lsp.util.make_range_params() + opts.lsp_params.context = { + diagnostics = vim.lsp.diagnostic.get_line_diagnostics() + } + + -- we use `vim.ui.select` for neovim > 0.6 + -- so make sure 'set_lsp_fzf_fn' is run synchronously + if vim.fn.has('nvim-0.6') == 1 then + opts.sync, opts.async = true, false + end + + -- when 'opts.sync == true' calls 'vim.lsp.buf_request_sync' + -- so we can avoid calling 'ui_select.register' when no code + -- actions are available + opts = set_lsp_fzf_fn(opts) + + -- error or no sync request no results + if not opts.fzf_fn then return end + -- use `vim.ui.select` for neovim > 0.6 -- the original method is now deprecated if vim.fn.has('nvim-0.6') == 1 then @@ -357,15 +380,11 @@ M.code_actions = function(opts) end ui_select.register(opts, true) vim.lsp.buf.code_action() + -- vim.defer_fn(function() + -- ui_select.deregister({}, true, true) + -- end, 100) return end - -- irrelevant for code actions and can cause - -- single results to be skipped with 'async = false' - opts.jump_to_single_result = false - opts.lsp_params = vim.lsp.util.make_range_params() - opts.lsp_params.context = { - diagnostics = vim.lsp.diagnostic.get_line_diagnostics() - } -- see discussion in: -- https://github.com/nvim-telescope/telescope.nvim/pull/738 @@ -451,10 +470,6 @@ M.code_actions = function(opts) opts.fzf_opts["--no-multi"] = '' opts.fzf_opts["--preview-window"] = 'right:0' opts.fzf_opts["--delimiter"] = vim.fn.shellescape(':') - opts = set_lsp_fzf_fn(opts) - - -- error or no sync request no results - if not opts.fzf_fn then return end core.fzf_wrap(opts, opts.fzf_fn, function(selected) diff --git a/lua/fzf-lua/providers/ui_select.lua b/lua/fzf-lua/providers/ui_select.lua index d03c176..1e9f2fd 100644 --- a/lua/fzf-lua/providers/ui_select.lua +++ b/lua/fzf-lua/providers/ui_select.lua @@ -8,6 +8,10 @@ local M = {} local _opts = nil local _old_ui_select = nil +M.is_registered = function() + return vim.ui.select == M.ui_select +end + M.deregister = function(_, silent, noclear) if not _old_ui_select then if not silent then @@ -83,7 +87,7 @@ M.ui_select = function(items, opts, on_choice) _opts = _opts or {} _opts.fzf_opts = { ['--no-multi'] = '', - ['--prompt'] = prompt:gsub(":$", "> "), + ['--prompt'] = prompt:gsub(":%s?$", "> "), ['--preview-window'] = 'hidden:right:0', }