Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Feature Request: Breakpoint persistence #198

Open
yingzhu146 opened this issue Jun 4, 2021 · 16 comments
Open

Feature Request: Breakpoint persistence #198

yingzhu146 opened this issue Jun 4, 2021 · 16 comments

Comments

@yingzhu146
Copy link

Hi there,

Would be great if I could persist the breakpoints over sessions - I often have to restart nvim (esp. because the dap UI is still a bit brittle over .stop() and .start() rcarriga/nvim-dap-ui#18. Persisting this over sessions would be great! :)

thanks a lot this awesome plugin.

@mfussenegger
Copy link
Owner

You could write some custom functions that let you export and import the active breakpoints:

local breakpoints = require('dap.breakpoints')


function M.store()
  local bps = {}
  local breakpoints_by_buf = breakpoints.get()
  for buf, buf_bps in pairs(breakpoints_by_buf) do
    bps[tostring(buf)] = buf_bps
  end
  local fp = io.open('/tmp/breakpoints.json', 'w')
  fp:write(vim.fn.json_encode(bps))
  fp:close()
end


function M.load()
  local fp = io.open('/tmp/breakpoints.json', 'r')
  local content = fp:read('*a')
  local bps = vim.fn.json_decode(content)
  for buf, buf_bps in pairs(bps) do
    for _, bp in pairs(buf_bps) do
      local line = bp.line
      local opts = {
        condition = bp.condition,
        log_message = bp.logMessage,
        hit_condition = bp.hitCondition
      }
      breakpoints.set(opts, tonumber(buf), line)
    end
  end
end

Not sure if this is a common enough use-case to warrant including something like that out of the box. (And the dap.breakpoints API is currently considered internal - meaning I might break BWC without going through a deprecation phase)

@yingzhu146
Copy link
Author

Thanks for the pointer and implementation suggestion here! I'm fine having a local function and dealing with breakages. Nonetheless I do think normal IDEs persist by default (although it's been a while so might be wrong here) so my biased opinion is that this should likely be default behaviour :) In any event, this solves my issue so thanks a lot, and feel free to notfix or close this issue if you don't think you'll implement it!

@rmagillxyz
Copy link

rmagillxyz commented Jun 8, 2021

Just my two cents, I think persisting breakpoints would be great. At least for the life of the current open file.

@mfussenegger
Copy link
Owner

At least for the life of the current open file.

This is currently already the case, or maybe I misunderstand what you mean with persisting?

@jrmoulton
Copy link

I'm all for persistent breakpoints! I think it would make sense to make it an optional feature that is off by default but having the option built in would be great!

@jrmoulton
Copy link

jrmoulton commented Feb 10, 2022

If anyone is interested I've added to the functions that @mfussenegger left in a comment so that the breakpoints persist even when neovim is closed and the numbers of the buffers change.

HOME = os.getenv("HOME")
local breakpoints = require('dap.breakpoints')

function _G.store_breakpoints(clear)
    local load_bps_raw = io.open(HOME .. '/.cache/dap/breakpoints.json', 'r'):read("*a")
    local bps = vim.fn.json_decode(load_bps_raw)
    local breakpoints_by_buf = breakpoints.get()
    if (clear) then
        for _, bufrn in ipairs(vim.api.nvim_list_bufs()) do
            local file_path = vim.api.nvim_buf_get_name(bufrn)
            if (bps[file_path] ~= nil) then
                bps[file_path] = {}
            end
        end
    else
        for buf, buf_bps in pairs(breakpoints_by_buf) do
            bps[vim.api.nvim_buf_get_name(buf)] = buf_bps
        end
    end
    local fp = io.open(HOME .. '/.cache/dap/breakpoints.json', 'w')
    local final = vim.fn.json_encode(bps)
    fp:write(final)
    fp:close()
end

function _G.load_breakpoints()
    local fp = io.open(HOME .. '/.cache/dap/breakpoints.json', 'r')
    local content = fp:read('*a')
    local bps = vim.fn.json_decode(content)
    local loaded_buffers = {}
    local found = false
    for _, buf in ipairs(vim.api.nvim_list_bufs()) do
        local file_name = vim.api.nvim_buf_get_name(buf)
        if (bps[file_name] ~= nil and bps[file_name] ~= {}) then
            found = true
        end
        loaded_buffers[file_name] = buf
    end
    if (found == false) then
        return
    end
    for path, buf_bps in pairs(bps) do
        for _, bp in pairs(buf_bps) do
            local line = bp.line
            local opts = {
                condition = bp.condition,
                log_message = bp.logMessage,
                hit_condition = bp.hitCondition
            }
            breakpoints.set(opts, tonumber(loaded_buffers[path]), line)
        end
    end
end

Then I trigger the storing at the same time as a breakpoint toggle or clear

vim.keymap.set( {'n', 'i', 'v'}, '<F3>', '<cmd>lua require"dap".clear_breakpoints();store_breakpoints(true)<CR>' )
vim.keymap.set( {'n', 'i', 'v'}, '<F4>', '<cmd>lua require"dap".toggle_breakpoint();store_breakpoints(false)<CR>' )

and I load the breakpoints with an autocommand everytime a file opens

    autocmd BufRead * :lua load_breakpoints()

This could probably be cleaned up because this is my first time writing lua but so far its been working pretty well for me

@child404
Copy link

If anyone is interested I've added to the functions that @mfussenegger left in a comment so that the breakpoints persist even when neovim is closed and the numbers of the buffers change.

HOME = os.getenv("HOME")
local breakpoints = require('dap.breakpoints')

function _G.store_breakpoints(clear)
    local load_bps_raw = io.open(HOME .. '/.cache/dap/breakpoints.json', 'r'):read("*a")
    local bps = vim.fn.json_decode(load_bps_raw)
    local breakpoints_by_buf = breakpoints.get()
    if (clear) then
        for _, bufrn in ipairs(vim.api.nvim_list_bufs()) do
            local file_path = vim.api.nvim_buf_get_name(bufrn)
            if (bps[file_path] ~= nil) then
                bps[file_path] = {}
            end
        end
    else
        for buf, buf_bps in pairs(breakpoints_by_buf) do
            bps[vim.api.nvim_buf_get_name(buf)] = buf_bps
        end
    end
    local fp = io.open(HOME .. '/.cache/dap/breakpoints.json', 'w')
    local final = vim.fn.json_encode(bps)
    fp:write(final)
    fp:close()
end

function _G.load_breakpoints()
    local fp = io.open(HOME .. '/.cache/dap/breakpoints.json', 'r')
    local content = fp:read('*a')
    local bps = vim.fn.json_decode(content)
    local loaded_buffers = {}
    local found = false
    for _, buf in ipairs(vim.api.nvim_list_bufs()) do
        local file_name = vim.api.nvim_buf_get_name(buf)
        if (bps[file_name] ~= nil and bps[file_name] ~= {}) then
            found = true
        end
        loaded_buffers[file_name] = buf
    end
    if (found == false) then
        return
    end
    for path, buf_bps in pairs(bps) do
        for _, bp in pairs(buf_bps) do
            local line = bp.line
            local opts = {
                condition = bp.condition,
                log_message = bp.logMessage,
                hit_condition = bp.hitCondition
            }
            breakpoints.set(opts, tonumber(loaded_buffers[path]), line)
        end
    end
end

Then I trigger the storing at the same time as a breakpoint toggle or clear

vim.keymap.set( {'n', 'i', 'v'}, '<F3>', '<cmd>lua require"dap".clear_breakpoints();store_breakpoints(true)<CR>' )
vim.keymap.set( {'n', 'i', 'v'}, '<F4>', '<cmd>lua require"dap".toggle_breakpoint();store_breakpoints(false)<CR>' )

and I load the breakpoints with an autocommand everytime a file opens

    autocmd BufRead * :lua load_breakpoints()

This could probably be cleaned up because this is my first time writing lua but so far its been working pretty well for me

Hey! Thanks for your solution! I faced some bugs with this implementation, so I decided to rewrite it in a more concise manner. Please, check it out in my repo if you are interested

@jrmoulton
Copy link

Haha I'll have to check it out because I've also had annoying bugs and have been too lazy to fix them. Thanks!

@Weissle
Copy link

Weissle commented Jul 11, 2022

I write a lua plugin for persistent checkpoints. https://github.com/Weissle/persistent-breakpoints.nvim

@matu3ba
Copy link

matu3ba commented Oct 19, 2022

Why not provide commands to export and import breakpoints? A better solution would be to record the actions in nvim-dap and let the user edit them.

The ideal solution can transpile the simple things between gdb/lldb and nvim-dap (probably only in one direction is feasible).

@BlueDrink9
Copy link

BlueDrink9 commented Dec 28, 2022

I'd like to suggest an alternative: set viminfo+=! and save breakpoints in a dictionary with all-caps name, eg

let g:DAPBREAKPOINTS={
\ "filepath"= { line = "condition"}
\}

That would save them to the viminfo file, which is where similar things should be stored and means users don't have another file cluttering things up - one which may not be writable if the code is in a system library.

https://learnvim.irian.to/basics/views_sessions_viminfo

@enesaltinkaya

This comment was marked as spam.

@orhnk
Copy link

orhnk commented Sep 25, 2023

@child404 Here is the linux version that creates the cache file if not exists:

M.store_breakpoints = function(clear)
  -- if doesn't exist create it:
  if vim.fn.filereadable(HOME .. "/.cache/dap/breakpoints.json") == 0 then
    -- Create file
    os.execute("mkdir -p " .. HOME .. "/.cache/dap")
    os.execute("touch " .. HOME .. "/.cache/dap/breakpoints.json")
  end

  local load_bps_raw = io.open(HOME .. "/.cache/dap/breakpoints.json", "r"):read "*a"
  if load_bps_raw == "" then
    load_bps_raw = "{}"
  end

  local bps = vim.fn.json_decode(load_bps_raw)
  local breakpoints_by_buf = require("dap.breakpoints").get()
  if clear then
    for _, bufrn in ipairs(vim.api.nvim_list_bufs()) do
      local file_path = vim.api.nvim_buf_get_name(bufrn)
      if bps[file_path] ~= nil then
        bps[file_path] = {}
      end
    end
  else
    for buf, buf_bps in pairs(breakpoints_by_buf) do
      bps[vim.api.nvim_buf_get_name(buf)] = buf_bps
    end
  end
  local fp = io.open(HOME .. "/.cache/dap/breakpoints.json", "w")
  local final = vim.fn.json_encode(bps)
  fp:write(final)
  fp:close()
end

M.load_breakpoints = function()
  local fp = io.open(HOME .. "/.cache/dap/breakpoints.json", "r")
  if fp == nil then
    print "No breakpoints found."
    return
  end
  local content = fp:read "*a"
  local bps = vim.fn.json_decode(content)
  local loaded_buffers = {}
  local found = false
  for _, buf in ipairs(vim.api.nvim_list_bufs()) do
    local file_name = vim.api.nvim_buf_get_name(buf)
    if bps[file_name] ~= nil and bps[file_name] ~= {} then
      found = true
    end
    loaded_buffers[file_name] = buf
  end
  if found == false then
    return
  end
  for path, buf_bps in pairs(bps) do
    for _, bp in pairs(buf_bps) do
      local line = bp.line
      local opts = {
        condition = bp.condition,
        log_message = bp.logMessage,
        hit_condition = bp.hitCondition,
      }
      require("dap.breakpoints").set(opts, tonumber(loaded_buffers[path]), line)
    end
  end
end

@yodho1psnrls
Copy link

There is a plugin with one of its features being exactly that, to save the breakpoints across sessions
https://github.com/daic0r/dap-helper.nvim

@tobihans
Copy link

tobihans commented Oct 20, 2024

For people using resession.nvim, I wrote this little extension to persist it. You can save it as lua/resession/extensions/dap.lua, and enable it in your resession config with {..., extensions = {..., dap = {}}}.

local M = {}

---Get the saved data for this extension
---@param _opts resession.Extension.OnSaveOpts Information about the session being saved
---@return any
M.on_save = function(_opts)
  local breakpoints = {}

  for bufnr, buf_breakpoints in pairs(require("dap.breakpoints").get()) do
    breakpoints[vim.api.nvim_buf_get_name(bufnr)] = buf_breakpoints
  end

  return breakpoints
end

---Restore the extension state
---@param breakpoints The value returned from on_save
M.on_post_load = function(breakpoints)
  local set = require("dap.breakpoints").set

  -- Build a table of <filename, buffer number>
  local loaded_buffers = {}
  vim.iter(vim.api.nvim_list_bufs()):each(function(bufnr)
    if vim.api.nvim_buf_is_loaded(bufnr) then
      local fname = vim.api.nvim_buf_get_name(bufnr)
      loaded_buffers[fname] = bufnr
    end
  end)

  -- Iterate over the breakpoints and restore them
  vim.iter(breakpoints):each(function(fname, buf_breakpoints)
    local bufnr = loaded_buffers[fname]
    if bufnr ~= nil then
      vim.iter(buf_breakpoints):each(
        function(bp)
          set({
            condition = bp.condition,
            log_message = bp.logMessage,
            hit_condition = bp.hitCondition,
          }, tonumber(bufnr), bp.line)
        end
      )
    end
  end)
end

return M

@dennypenta
Copy link

dennypenta commented Dec 1, 2024

there is an issue with this approach I can't find how to fix.
restoring breakpoints from a file can't be consistent.
let's say I have a session and open a couple buffers (b1 and b2) leaving a few breakpoints in there.
then closing either buffer, exit vim an restoring the session is impossible, dap.breakpoints.set doesn't support breakpoints toggling on a closed buffer, so in my new session or a restored one I have to open all the buffers from my json and then set the breakpoints.
is there a simpler solution? do i do anything wrong?

another issue in this approach with modules update.
If I install a new version of a dependency the internal path to the file doesn't change, but the content may change dramatically, so it means all my breakpoints in this specific subpath (module I updated) must be removed.
I guess it can be a function that will clean the breakpoints in a given directory. Im not sure if a buffer path/number would be changed in this case if there an easy way to detect it or only call it automatically

tldr;
dap.breakpoints.set doesn't work for not loaded buffers

# for free to join this conversation on GitHub. Already have an account? # to comment
Projects
None yet
Development

No branches or pull requests