local utils = require("debugmaster.utils")
local api = vim.api
local M = {}

---@class dm.KeySpec
---@field key string
---@field action fun()
---@field desc string?
---@field nowait boolean?
---@field group string?
---@field modes table | nil Table with modes like in vim.keymap.set. {"n"} by default

---@class dm.MappingsGroup
---@field name string? Group name. Not shown in HelpPopup if nil
---@field hlgroup string? Define name highlight color in HelpPopup
---@field mappings dm.KeySpec[]

---@type dm.MappingsGroup
local move_debugger_group = {
  name = "MOVE DEBUGGER",
  hlgroup = "ERROR",
  -- the main goal when developing keymaps for debug mode is to not override normal mode motions
  -- We want to our DEBUG mode be "constant". So we can freely move and can't edit text
  mappings = {
    {
      key = "o",
      action = function() require("dap").step_over() end,
      desc = "Step over. Works with count (Try to type '5o')",
    },
    {
      key = "m",
      action = function() require("dap").step_into() end,
      desc = "Step into (mine deeper)",
    },
    {
      key = "q",
      action = function() require("dap").step_out() end,
      desc = "Step out ([q]uit current stack frame)",
    },
    {
      key = "c",
      nowait = true,
      action = function() require("dap").continue() end,
      desc = "Continue or start debug session",
    },
    {
      key = "r",
      action = function() require("dap").run_to_cursor() end,
      desc = "Run to cursor",
    },
  }
}

---@type dm.MappingsGroup
local sidepanel = {
  name = "SIDEPANEL",
  hlgroup = "STRING",
  mappings = {
    {
      key = "u",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:toggle()
      end,
      desc = "Toggle ui",
    },
    {
      key = "U",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:toggle_layout()
      end,
      desc = "Toggle ui float mode",
    },
    {
      key = "S",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:set_active_with_open(state.scopes)
      end,
      desc = "Open scopes (global, local, etc variables)",
    },
    {
      key = "T",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:set_active_with_open(state.terminal)
      end,
      desc = "Open terminal",
    },
    {
      key = "R",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:set_active_with_open(state.repl)
      end,
      desc = "Open repl",
    },
    {
      key = "H",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:set_active_with_open(state.help)
      end,
      desc = "Open help",
    },
    {
      key = "}",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:rotate(1)
      end,
      desc = "Rotate sidenapel clockwise",
    },
    {
      key = "{",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:rotate(-1)
      end,
      desc = "Rotate sidenapel anticlockwise",
    },
    {
      key = "-",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:resize(-10)
      end,
      desc = "Decrease sidenapel size",
    },
    {
      key = "+",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:resize(10)
      end,
      desc = "Increase sidepanel size",
    }
  }
}

---@type dm.MappingsGroup
local float_widgets = {
  name = "FLOAT WIDGETS",
  hlgroup = "STATEMENT",
  mappings = {
    {
      key = "df",
      action = function()
        local widgets = require("dap.ui.widgets")
        pcall(widgets.cursor_float, widgets.frames)
        utils.register_to_close_on_leave(api.nvim_get_current_win())
      end,
      desc = "Frames widget"
    },
    {
      key = "dt",
      action = function()
        local widgets = require("dap.ui.widgets")
        pcall(widgets.cursor_float, widgets.threads)
        utils.register_to_close_on_leave(api.nvim_get_current_win())
      end,
      desc = "Threads widget"
    },
    {
      key = "ds",
      action = function()
        local widgets = require("dap.ui.widgets")
        local ok, sessions = pcall(widgets.cursor_float, widgets.sessions)
        if not ok then
          return
        end
        utils.register_to_close_on_leave(api.nvim_get_current_win())
        vim.keymap.set("n", "<CR>", vim.schedule_wrap(function()
          require('dap.ui').trigger_actions({ mode = 'first' })
          api.nvim_exec_autocmds("User", { pattern = "DapSessionChanged" })
          require("dap").focus_frame()
        end), { expr = true, buffer = sessions.buf })
      end,
      desc = "Debug sessions widget",
    },
    {
      key = "db",
      action = function()
        local state = require("debugmaster.state")
        utils.open_floating_window(state.breakpoints.buf, {
          min_width = 60,
          additional_height = 3,
        })
        utils.register_to_close_on_leave(api.nvim_get_current_win())
        vim.bo[state.breakpoints.buf].filetype = "dap-float"
      end,
      desc = "Breakpoints widget"
    },
    {
      key = "I",
      modes = { "n", "v" },
      action = function()
        pcall(require('dap.ui.widgets').hover)
        utils.register_to_close_on_leave(api.nvim_get_current_win())
      end,
      desc = "Inspect variable or visually selected expression",
    },
  }
}

---@type dm.MappingsGroup
local breakpoings_group =

---@type dm.MappingsGroup
local misc_group = {
  name = "MISCELANOUS",
  hlgroup = "TYPE",
  mappings = {
    {
      key = "dr",
      desc = "Restart the current session or rerun last if none",
      action = function()
        require("debugmaster.plugins").plugins.last_config_rerunner.run_last_cached()
      end
    },
    {
      key = "dn",
      action = function()
        vim.cmd("DapNew")
      end,
      desc = "Debug start new sessions",
    },
    {
      key = "dq",
      action = function()
        require("dap").terminate()
        local state = require("debugmaster.state")
        state.sidepanel:close()
      end,
      desc = "Quit debug"
    },
    {
      key = "[s",
      action = function() require("dap").down() end,
      desc = "Go to previous stack frame"
    },
    {
      key = "]s",
      action = function() require("dap").up() end,
      desc = "Go to next stack frame"
    },
    {
      key = "dj",
      action = function() require("dap").focus_frame() end,
      desc = "Jump to the current stack frame"
    },
    {
      key = "x",
      action = function()
        local state = require("debugmaster.state")
        local text = vim.fn.getreg('"')
        require("dap").repl.execute("\n" .. text)
        state.sidepanel:set_active_with_open(state.repl)
        api.nvim_buf_call(state.repl.buf, function()
          vim.cmd("normal G")
        end)
      end,
      desc = "Execute last yanked or deleted text in the repl",
    },
    {
      key = "dm",
      action = function()
        local state = require("debugmaster.state")
        local terminal = state.terminal
        local buf = api.nvim_get_current_buf()
        local is_term = api.nvim_get_option_value('buftype', { buf = buf }) == 'terminal'
        if not is_term then
          return print("Current buffer isn't terminal. Can't move to the Terminal section")
        end
        local ok = terminal:attach_terminal_to_current_session(buf)
        if ok then
          for _, win in ipairs(utils.get_windows_for_buffer(buf)) do
            api.nvim_win_close(win, true)
          end
          state.sidepanel:open()
          state.sidepanel:set_active(terminal)
        end
      end,
      desc = "Move current terminal to the 'Terminal' segment"
    },
  }
}

---@type dm.MappingsGroup[]
M.groups = {
  move_debugger_group,
  breakpoings_group,
  sidepanel,
  float_widgets,
  misc_group,
}

---Give the reference to the key entry so you can remap it to something else
---Throws an error if the key doesn't exist
---@return dm.KeySpec
function M.get(key)
  for _, group in ipairs(M.groups) do
    for _, mapping in ipairs(group.mappings) do
      if mapping.key == key then
        return mapping
      end
    end
  end
  error("Key doesn't exist")
end

--- Add new user mapping to the MISCELANOUS group
---@param mapping dm.KeySpec
function M.add(mapping)
  table.insert(misc_group.mappings, mapping)
end

return M

{
  name = "BREAKPOINTS",
  hlgroup = "Boolean",
  mappings = {
    {
      key = "t",
      action = function() require("dap").toggle_breakpoint() end,
      desc = "Toggle breakpoint",
    },
    {
      key = "da",
      action = function()
        require("dap").clear_breakpoints()
        print("All breakpoints removed")
      end,
      desc = "Delete all breakpoints",
    },
    {
      key = "dc",
      action = function()
        local condition = vim.fn.input({ prompt = "Enter breakpoing condition: " })
        if condition ~= "" then
          require("dap").toggle_breakpoint(condition)
        end
      end,
      desc = "Add conditional breakpoint",
    },
    {
      key = "[b",
      action = function() require("debugmaster.utils").gotoBreakpoint("prev") end,
      desc = "Go to previous breakpoint"
    },
    {
      key = "]b",
      action = function() require("debugmaster.utils").gotoBreakpoint("next") end,
      desc = "Go to next breakpoint"
    },
  },
}

local api = vim.api
local M = {}

local plugins = {}

---@class dm.Plugin
---@field activate fun() internal plugin function. User shouldn't call it
---@field enabled boolean? nil means plugin is enabled

plugins.cursor_hl = (function()
  ---@type dm.Plugin
  local plugin = {
    activate = function()
      local dcursor = api.nvim_get_hl(0, { name = "dCursor" })
      -- https://stackoverflow.com/questions/1252539/most-efficient-way-to-determine-if-a-lua-table-is-empty-contains-no-entries
      if next(dcursor) == nil then
        api.nvim_set_hl(0, "dCursor", { bg = "#2da84f" })
      end

      local cursor_mode_off = vim.o.guicursor
      local cursor_mode_on = cursor_mode_off .. ",a:dCursor"

      api.nvim_create_autocmd("User", {
        pattern = "DebugModeChanged",
        callback = function(args)
          if args.data.enabled then
            vim.go.guicursor = cursor_mode_on
          else
            vim.go.guicursor = cursor_mode_off
          end
        end
      })
    end
  }
  return plugin
end)()

plugins.ui_auto_toggle = (function()
  ---@type dm.Plugin
  local plugin = {
    activate = function()
      local dap = require("dap")
      dap.listeners.before.launch["debugmaster"] = function()
        require("debugmaster.state").sidepanel:open()
      end

      dap.listeners.before.attach["debugmaster"] = function()
        require("debugmaster.state").sidepanel:open()
      end

      dap.listeners.before.event_terminated["debugmaster"] = function()
        require("debugmaster.state").sidepanel:close()
        print("dap terminated")
      end

      dap.listeners.before.event_exited["debugmaster"] = function()
        require("debugmaster.state").sidepanel:close()
        print("dap exited")
      end

      dap.listeners.before.disconnect["debugmaster"] = function()
        require("debugmaster.state").sidepanel:close()
        print("dap disconnected")
      end
    end
  }
  return plugin
end)()

-- probably this code doesn't fit in the plugins concept. Need to move it somewhere
plugins.last_config_rerunner = (function()
  local dap = require("dap")
  local session_configs = {}
  local last_config = nil
  ---@type dm.Plugin
  local plugin = {
    activate = function()
      dap.listeners.after.event_initialized["dm-saveconfig"] = function(session)
        local config = session.config
        last_config = config
        session_configs[session.id] = config
      end
    end,
    run_last_cached = function()
      local session = require("dap").session()
      if session then
        local config = assert(session_configs[session.id], "Active session exist, but config doesn't. Strange...")
        return dap.run(config)
      elseif last_config then
        return dap.run(last_config)
      end
      print("No configuration available to re-run")
    end
  }
  return plugin
end)()

---@type dm.Plugin
plugins.dap_float_close_on_q = {
  activate = function()
    api.nvim_command 'autocmd FileType dap-float nnoremap <buffer><silent> q <cmd>close!<CR>'
  end,
}

---@type dm.Plugin
plugins.osv_integration = {
  enabled = false,
  activate = function()
    local dap = require("dap")
    local instance_n = 1
    dap.adapters.debugmasterosv = (function()
      local id = "nvimdebug"
      local buf = -1
      dap.listeners.after.disconnect[id] = function()
        pcall(vim.api.nvim_buf_delete, buf, { force = true, unload = true })
      end
      local function find_module_path(name, no_suffix)
        local suffixes = { string.format("lua/%s.lua", name), string.format("lua/%s/init.lua", name) }
        for _, suf in ipairs(suffixes) do
          local path = vim.api.nvim_get_runtime_file(suf, false)[1]
          if path then
            path = vim.fs.normalize(path)
            return no_suffix and path:match("(.+)" .. suf) or path
          end
        end
      end
      return function(callback)
        -- we can't capture even integers for the function we dump
        -- hence using this json trick
        ---@param vars init-vars
        local init = function(vars)
          vim.opt.rtp:prepend(vars.dap_path)
          vim.opt.rtp:prepend(vars.osv_path)
          -- disable output because it significantly degrade performance
          require('osv').launch({ blocking = true, port = vars.port, output = false })
        end
        ---@class init-vars
        local vars = {
          port = math.random(49152, 65535),
          osv_path = assert(find_module_path("osv", true), "abort: one-small-step-for-vimkind not installed!!!"),
          dap_path = assert(find_module_path("dap", true), "abort: dap not installed!!!"),
        }
        -- https://gist.github.com/veechs/bc40f1f39b30cb1251825f031cd6d978
        local cmd = string.format(
          [[split | terminal nvim --cmd "lua loadstring( vim.base64.decode('%s') )( vim.json.decode(vim.base64.decode('%s')) )"]],
          vim.base64.encode(string.dump(init)), vim.base64.encode(vim.json.encode(vars))
        )
        vim.cmd(cmd)
        buf = vim.api.nvim_get_current_buf()
        api.nvim_buf_set_name(buf, "nvim-debug-" .. instance_n)
        instance_n = instance_n + 1
        vim.api.nvim_win_close(0, true)
        dap.listeners.after.initialize[id] = function()
          local state = require("debugmaster.state")
          state.terminal:attach_terminal_to_current_session(buf)
          state.sidepanel:set_active(state.terminal)
          dap.listeners.after.initialize[id] = nil
        end
        callback({ type = 'server', host = "127.0.0.1", port = vars.port })
      end
    end)()

    dap.configurations.lua = dap.configurations.lua or {}
    table.insert(dap.configurations.lua, {
      type = 'debugmasterosv',
      request = 'attach',
      name = "Debug neovim (lua). Provided by debugmaster.nvim",
    })
  end
}

local plugins_enabled = false
M.init = function()
  if not plugins_enabled then
    for _, plugin in pairs(M.plugins) do
      if plugin.enabled == nil or plugin.enabled then
        plugin.activate()
      end
    end
    plugins_enabled = true
  end
end

M.plugins = plugins

return M

---@class dm.debug.mode
local M = {}

local active = false
local groups = require("debugmaster.debug.keymaps").groups
local api = vim.api

---@class dm.OrignalKeymap
---@field callback function?
---@field rhs string?
---@field desc string?
---@field silent boolean?

---@type table<string, table<string, dm.OrignalKeymap>> [mode: {key: OriginalKeymap}, ...]
local originals = {}

local function save_original_settings()
  local all = {
    n = api.nvim_get_keymap("n"),
    v = api.nvim_get_keymap("v"),
  }
  local lhs_to_map = {}

  for mode, mappings in pairs(all) do
    lhs_to_map[mode] = {}
    for _, mapping in ipairs(mappings) do
      lhs_to_map[mode][mapping.lhs] = mapping
    end
  end

  for _, group in ipairs(groups) do
    for _, mapping in ipairs(group.mappings) do
      for _, mode in ipairs(mapping.modes or { "n" }) do
        local key = mapping.key
        if not originals[mode] then
          originals[mode] = {}
        end
        local orig = lhs_to_map[mode][key] or {}
        originals[mode][key] = {
          callback = orig.callback,
          rhs = orig.rhs,
          desc = orig.desc,
          silent = orig.silent,
        }
      end
    end
  end
end

M.enable = (function()
  local is_first_enable = true
  return function()
    if is_first_enable then
      save_original_settings()
      is_first_enable = false
    end
    if active then
      return
    end
    active = true
    for _, group in ipairs(groups) do
      for _, mapping in ipairs(group.mappings) do
        local action = mapping.action
        for _, mode in ipairs(mapping.modes or { "n" }) do
          vim.keymap.set(mode, mapping.key, action, { nowait = mapping.nowait })
        end
      end
    end
    api.nvim_exec_autocmds("User", { pattern = "DebugModeChanged", data = { enabled = true } })
  end
end)()

function M.disable()
  if not active then
    return
  end
  active = false
  for _, group in ipairs(groups) do
    for _, mapping in ipairs(group.mappings) do
      local key = mapping.key
      for _, mode in ipairs(mapping.modes or { "n" }) do
        local orig = originals[mode][key]
        local rhs = orig.callback or orig.rhs or key
        vim.keymap.set(mode, key, rhs, {
          desc = orig.desc,
          silent = orig.silent,
        })
      end
    end
  end
  api.nvim_exec_autocmds("User", { pattern = "DebugModeChanged", data = { enabled = false } })
end

function M.toggle()
  (active and M.disable or M.enable)()
end

function M.is_active()
  return active
end

return M

local utils = require("debugmaster.utils")
local api = vim.api

---@class dm.ui.Sidepanel.IComponent
---@field name string
---@field buf number

---@class dm.ui.Sidepanel
local Sidepanel = {}

function Sidepanel.new()
  ---@class dm.ui.Sidepanel
  local self = setmetatable({}, { __index = Sidepanel })
  self.win = -1 -- always need to check if valid before doing something
  self.direction = "right"
  self.float = false
  self.size = 50 -- size for the split. not applied to float. always in the range [10;90]

  ---@type dm.ui.Sidepanel.IComponent[]
  self.components = {}
  ---@type dm.ui.Sidepanel.IComponent
  self.active = nil

  api.nvim_create_autocmd("User", {
    pattern = "WidgetBufferNumberChanged",
    callback = vim.schedule_wrap(function()
      if self.active and self:is_open() then
        api.nvim_win_set_buf(self.win, self.active.buf)
        self:_cook_winbar()
      end
    end)
  })

  api.nvim_create_autocmd("VimResized", {
    callback = function()
      if self:is_open() then
        self:close()
        self:open()
      end
    end
  })

  return self
end

function Sidepanel:is_open()
  return api.nvim_win_is_valid(self.win)
end

function Sidepanel:is_focused()
  return api.nvim_get_current_win() == self.win
end

function Sidepanel:toggle()
  if not self:is_open() then
    self:open()
  else
    self:close()
  end
end

---@class dm.ui.Sidepanel.OpenOptions
---@field direction "left" | "right" | "above" | "below" | nil Opens in previous state if nil (right when open first time)
---@field float boolean? If float specified then it creates float window and ignore direction

---@param opts dm.ui.Sidepanel.OpenOptions?
function Sidepanel:open(opts)
  if self:is_open() then
    return
  end

  -- preparing new options (float, direction)
  opts = opts or {}
  local float = self.float
  if opts.float ~= nil then
    float = opts.float
  end
  local direction = opts.direction or self.direction
  ---@type vim.api.keyset.win_config
  local cfg = {}
  local enter = false

  if float then
    cfg = utils.make_center_float_win_cfg()
    enter = true
  else
    cfg.split = direction
    cfg.win = -1
    cfg.width = math.floor(vim.o.columns  * (self.size / 100))
    cfg.height = math.floor(vim.o.lines  * (self.size / 100))
  end

  --  it saves us if we try open it in a float window
  local ok, res = pcall(api.nvim_open_win, self.active.buf, enter, cfg)
  if not ok then
    print("Can't open window.", res)
    return
  end

  -- Applying new options (if pcall successfull)
  self.win = res
  self.direction = direction
  self.float = float
  api.nvim_set_option_value("number", false, { win = self.win })
  api.nvim_set_option_value("relativenumber", false, { win = self.win })
  if self.float then
    utils.register_to_close_on_leave(self.win)
  end
  self:_cook_winbar()
end

function Sidepanel:_cook_winbar()
  -- TODO: redraw on resize
  local win_width = api.nvim_win_get_width(self.win)
  local join_text_width = 0
  for _, comp in ipairs(self.components) do
    join_text_width = join_text_width + #comp.name
  end
  local max_indent = math.floor((win_width - join_text_width) / #self.components)
  local indent = string.rep(" ", max_indent)

  local winbar = {}
  for i, comp in ipairs(self.components) do
    local text = comp.name
    -- Each clickable region needs a unique identifier (using index i)
    text = string.format("%%%d@v:lua.DebugmasterClickWinbar@%s%%*", i, text)

    if comp == self.active then
      -- Apply highlight after the clickable region
      text = utils.status_line_apply_hl(text, "Exception")
    end

    table.insert(winbar, text)
  end

  vim.wo[self.win].winbar = table.concat(winbar, indent)
end

function DebugmasterClickWinbar(index)
  -- this code is absolutely cursed, don't want to even speak about this...
  local self = require("debugmaster.state").sidepanel
  local comp = self.components[index]
  self:set_active(comp)
end

function Sidepanel:close()
  if self:is_open() then
    -- may fail if trying to close last window
    pcall(api.nvim_win_close, self.win, true)
  end
end

---rotate sidebar clockwise
---@param step number
function Sidepanel:rotate(step)
  if not api.nvim_win_is_valid(self.win) or self.float then
    return
  end
  local was_focused = self:is_focused()
  local cur_direction = self.direction
  self:close()
  local directions = { "below", "left", "above", "right" }
  for i, direction in ipairs(directions) do
    if direction == cur_direction then
      -- let's pretend array starts with zero (hence i - 1)
      local index = ((i - 1) + step) % #directions
      local next = directions[index + 1]
      self:open({ direction = next })
    end
  end
  if was_focused then
    api.nvim_set_current_win(self.win)
  end
end

function math.clamp(n, low, high)
  return math.min(math.max(n, low), high)
end

function Sidepanel:resize(step)
  if self.float or not self:is_open() then
    return
  end
  self.size = math.clamp(self.size + step, 10, 90)
  self:close()
  self:open()
end

function Sidepanel:toggle_layout()
  if not self:is_open() then
    self:open()
  end
  self:close()
  self:open({ float = not self.float })
end

---@param comp dm.ui.Sidepanel.IComponent
function Sidepanel:set_active(comp)
  self.active = comp
  if self:is_open() then
    api.nvim_win_set_buf(self.win, self.active.buf)
    self:_cook_winbar()
  end
end

-- if this comp already active then it do nothing
-- set comp as active and open panel if it is closed
---@param comp dm.ui.Sidepanel.IComponent
function Sidepanel:set_active_with_open(comp)
  if self.active == comp and self:is_open() then
    return
  end
  self:set_active(comp)
  self:open()
end

---@param comp dm.ui.Sidepanel.IComponent
function Sidepanel:add_component(comp)
  table.insert(self.components, comp)
end

return Sidepanel

local dap = require("dap")
local api = vim.api

---@class dm.ui.Terminal: dm.ui.Sidepanel.IComponent
local Terminal = {}

local terms_per_session = {}

function Terminal.new()
  ---@class dm.ui.Terminal
  local self = setmetatable({}, { __index = Terminal })
  self.name = "[T]erminal"

  self._dummy_buf = api.nvim_create_buf(false, true)
  self.buf = self._dummy_buf
  local lines = {
    "Debug adapter didn't provide terminal",
    "1. Either no session is active",
    "2. Eiter you attached to the process",
    "And then you can move the neovim terminal with the program",
    "to this section using 'dm' keymap in the debug mode",
    "3. Either you need to tweak your adapter configugration options",
    "And probably, the program output is being redirected to the REPL right now.",
    "- Consult with your debug adapter documentation",
    "https://github.com/mfussenegger/nvim-dap/wiki/Debug-Adapter-installation",
    'Usually required option is `console = "integratedTerminal"`',
    "- Check nvim dap issues about your debug adapter",
  }
  api.nvim_buf_set_lines(self.buf, 0, -1, false, lines)
  api.nvim_set_option_value("modifiable", false, { buf = self.buf })

  dap.defaults.fallback.terminal_win_cmd = function()
    local term_buf = api.nvim_create_buf(false, false)
    self:attach_terminal_to_current_session(term_buf)
    return term_buf, nil
  end

  api.nvim_create_autocmd("User", {
    pattern = "DapSessionChanged",
    callback = vim.schedule_wrap(function()
      local session = assert(dap.session())
      local term = terms_per_session[session.id] or self._dummy_buf
      self.buf = term
      api.nvim_exec_autocmds("User", { pattern = "WidgetBufferNumberChanged" })
    end),
  })

  return self
end

---@param buf number
---@return boolean indicate if attach was successful
function Terminal:attach_terminal_to_current_session(buf)
  local session = dap.session()
  if not session then
    print("Can't attach terminal. No active session")
    return false
  elseif terms_per_session[session.id] then
    print("Can't attach terminal. Already attached")
    return false
  end

  terms_per_session[session.id] = buf
  self.buf = buf
  api.nvim_exec_autocmds("User", { pattern = "WidgetBufferNumberChanged" })

  api.nvim_create_autocmd({ "BufDelete", "BufUnload" }, {
    callback = function(args)
      if args.buf == self.buf then
        self.buf = self._dummy_buf
        terms_per_session[session.id] = nil
        api.nvim_exec_autocmds("User", { pattern = "WidgetBufferNumberChanged" })
      end
    end
  })

  return true
end

return Terminal

local dap = require("dap")
local breakpoints = require("dap.breakpoints")
local Tree = require("debugmaster.ui.Tree")
local api = vim.api

---@class dm.BreakpointNode: dm.NodeTrait
local BreakpointNode = {}

---@param bpoints {buf: number, bpoints: any}?
function BreakpointNode.new(bpoints, bpoint)
  ---@class dm.BreakpointNode
  local self = setmetatable({}, { __index = BreakpointNode })
  self.id = "first"
  self.bpoints = bpoints
  self.bpoint = bpoint
  if bpoints or bpoint then
    self.id = bpoints and ("buf" .. bpoints.buf) or ("bp" .. bpoint.line)
  end
  return self
end

function BreakpointNode:get_children_iter()
  if not self.bpoint and not self.bpoints then
    return coroutine.wrap(function()
      for buf, bpoints in pairs(breakpoints.get()) do
        coroutine.yield(BreakpointNode.new({ buf = buf, bpoints = bpoints }))
      end
    end)
  elseif self.bpoints then
    return vim.iter(self.bpoints.bpoints):map(function(b)
      b.buf = self.bpoints.buf
      return BreakpointNode.new(nil, b)
    end)
  end
  return function() end
end

function BreakpointNode:get_repr()
  if not self.bpoints and not self.bpoint then
    local help = {
      { { "t - remove breakpoint or all breakpoints in the file", "Comment" } },
      { { "c - change breakpoint condition", "Comment" } }
    }
    return { { "Breakpoints", "Exception" } }, help
  elseif self.bpoints then
    local path = api.nvim_buf_get_name(self.bpoints.buf)
    path = vim.fn.fnamemodify(path, ":.")
    return { { path, "Statement" } }
  else
    local vlines = nil
    local indent = "    "
    local linenr = self.bpoint.line
    local line = vim.trim(api.nvim_buf_get_lines(self.bpoint.buf, linenr - 1, linenr, false)[1] or "")
    local text = string.format("%s %s %s", indent, linenr, line)
    local condition = self.bpoint.condition
    if condition and condition ~= "" then
      vlines = { { { indent }, { "condition: ", "Comment" }, { self.bpoint.condition } } }
    end
    return { { text } }, vlines
  end
end

function BreakpointNode:is_expanded()
  return true
end

---@class dm.ui.Breakpoints: dm.ui.Sidepanel.IComponent
local Breakpoints = {}

function Breakpoints.new()
  ---@class dm.ui.Breakpoints
  local self = setmetatable({}, { __index = Breakpoints })
  self.name = "[B]points"
  self._tree = Tree.new_with_buf(BreakpointNode.new())
  self.buf = self._tree.buf

  vim.keymap.set("n", "c", function()
    local line = api.nvim_win_get_cursor(0)[1] - 1
    ---@type dm.BreakpointNode?
    local node = self._tree:node_by_line(line)
    if node and node.bpoint then
      local bp = node.bpoint
      local condition = vim.fn.input({ default = bp.condition or "" })
      breakpoints.set({ condition = condition }, bp.buf, bp.line)
      self._tree:render()
    end
  end, { buffer = self.buf, nowait = true })

  vim.keymap.set("n", "t", function()
    local line = api.nvim_win_get_cursor(0)[1] - 1
    ---@type dm.BreakpointNode?
    local node = self._tree:node_by_line(line)
    if node then
      if node.bpoint then
        local bp = node.bpoint
        breakpoints.remove(bp.buf, bp.line)
        for _, session in pairs(dap.sessions()) do
          session:set_breakpoints(breakpoints.get(bp.buf))
        end
      elseif node.bpoints then
        local bps = node.bpoints or {}
        for _, bp in pairs(bps.bpoints) do
          breakpoints.remove(bps.buf, bp.line)
          for _, session in pairs(dap.sessions()) do
            session:set_breakpoints(breakpoints.get(bp.buf))
          end
        end
      end
      self._tree:render()
    end
  end, { buffer = self.buf, nowait = true })

  vim.keymap.set("n", "<CR>", function()
    local line = api.nvim_win_get_cursor(0)[1] - 1
    ---@type dm.BreakpointNode?
    local node = self._tree:node_by_line(line)
    if node then
      if node.bpoint then
        local bp = node.bpoint
        breakpoints.remove(bp.buf, bp.line)
        vim.cmd("q")
        vim.cmd("buffer " .. bp.buf)
        vim.cmd("normal " .. bp.line .. "G")
      end
    end
  end, { buffer = self.buf })

  dap.listeners.after.setBreakpoints["debugmaster"] = function()
    self._tree:render()
  end

  return self
end

return Breakpoints

local api = vim.api

local M = {}

-- https://github.com/mfussenegger/nvim-dap/issues/792
---@param dir "next"|"prev"
function M.gotoBreakpoint(dir)
  local breakpoints = require("dap.breakpoints").get()
  if #breakpoints == 0 then
    vim.notify("No breakpoints set", vim.log.levels.WARN)
    return
  end
  local points = {}
  for bufnr, buffer in pairs(breakpoints) do
    for _, point in ipairs(buffer) do
      table.insert(points, { bufnr = bufnr, line = point.line })
    end
  end

  local current = {
    bufnr = api.nvim_get_current_buf(),
    line = api.nvim_win_get_cursor(0)[1],
  }

  local nextPoint
  for i = 1, #points do
    local isAtBreakpointI = points[i].bufnr == current.bufnr and points[i].line == current.line
    if isAtBreakpointI then
      local nextIdx = dir == "next" and i + 1 or i - 1
      if nextIdx > #points then nextIdx = 1 end
      if nextIdx == 0 then nextIdx = #points end
      nextPoint = points[nextIdx]
      break
    end
  end
  if not nextPoint then nextPoint = points[1] end

  vim.cmd(("buffer +%s %s"):format(nextPoint.line, nextPoint.bufnr))
end

function M.make_center_float_win_cfg()
  local height = math.ceil(math.min(vim.o.lines, math.max(20, vim.o.lines - 5)))
  local width = math.ceil(math.min(vim.o.columns, math.max(80, vim.o.columns - 10)))
  ---@type vim.api.keyset.win_config
  local cfg = {
    relative = "editor",
    border = "rounded",
    width = width,
    height = height,
    row = math.ceil(vim.o.lines - height) * 0.5 - 1,
    col = math.ceil(vim.o.columns - width) * 0.5 - 1
  }
  return cfg
end

---@param win number
function M.register_to_close_on_leave(win)
  local id
  id = api.nvim_create_autocmd("WinLeave", {
    callback = function()
      if api.nvim_win_is_valid(win) then
        api.nvim_win_close(win, true)
      end
      api.nvim_del_autocmd(id)
    end
  })
end

-- https://www.reddit.com/r/neovim/comments/tz6p7i/how_can_we_set_color_for_each_part_of_statusline/
---@return string
function M.status_line_apply_hl(str, hlGroup)
  return "%#" .. hlGroup .. "#" .. str .. "%*"
end

function M.get_windows_for_buffer(buf)
  local windows = {}
  for _, win in ipairs(api.nvim_list_wins()) do
    if api.nvim_win_get_buf(win) == buf then
      table.insert(windows, win)
    end
  end
  return windows
end

function M.open_floating_window(buf, opts)
  local lines = api.nvim_buf_get_lines(buf, 0, -1, false)
  local width = 1
  for _, line in ipairs(lines) do
    width = math.max(width, vim.fn.strdisplaywidth(line), opts.min_width or 1)
  end
  local height = math.max(#lines, 1)

  ---@type vim.api.keyset.win_config
  local win_config = {
    row = 0,
    col = 0,
    relative = 'cursor',
    width = width,
    height = height + (opts.additional_height or 0),
    style = 'minimal',
    border = "rounded",
    focusable = true
  }

  -- Create and configure window
  local win = api.nvim_open_win(buf, true, win_config)
  return win
end

function M.debounce(fn, ms)
  local timer = assert(vim.uv.new_timer())
  return function(...)
    local args = { ... }
    timer:stop()
    timer:start(ms, 0, vim.schedule_wrap(function()
      fn(unpack(args))
    end))
  end
end

return M

local api = vim.api

---@alias dm.HlLine [string, string] First is text, second is highlight group
---@alias dm.NodeRepr nil | dm.HlLine[]
---@alias dm.ChildrenIter fun(): dm.NodeTrait?

---@class dm.NodeTrait Supposedly it should be template parameter to Tree but since lua-ls doesn't support generic classes...
---@field id string
---@field is_expanded fun(self): boolean
---@field get_children_iter fun(self, depth): dm.ChildrenIter
---@field get_repr fun(self, depth): dm.NodeRepr

---@class dm.Tree
local Tree = {}

function Tree.new(buf, root)
  ---@class dm.Tree
  local self = setmetatable({}, { __index = Tree })
  self.buf = buf
  self.root = root
  self._nodes_by_line = {}
  self._nodes_by_id = {}
  self._ns_id = api.nvim_create_namespace("")
  return self
end

function Tree.new_with_buf(root)
  return Tree.new(api.nvim_create_buf(false, true), root)
end

function Tree:render(start)
  api.nvim_set_option_value("modifiable", true, { buf = self.buf })
  self._nodes_by_line = {}
  self._nodes_by_id = {}
  local lines = {}
  local highlights = {}      -- {line, hl_group, start_col, end_col}
  local virt_line_marks = {} -- {line = N, lines = virt_lines}
  local line_num = 0

  local function render_node(node, depth)
    local segments, virtual_lines = node:get_repr(depth)
    segments = segments or {}

    -- Build line text and highlights
    local line_text = ""
    local current_col = 0
    for _, seg in ipairs(segments) do
      line_text = line_text .. seg[1]
      if seg[2] then
        table.insert(highlights, {
          line = line_num,
          hl = seg[2],
          start = current_col,
          ["end"] = current_col + #seg[1]
        })
      end
      current_col = current_col + #seg[1]
    end

    table.insert(lines, line_text)
    self._nodes_by_line[line_num] = node
    self._nodes_by_id[node.id] = node

    if virtual_lines and #virtual_lines > 0 then
      table.insert(virt_line_marks, {
        line = line_num,
        lines = virtual_lines
      })
    end

    line_num = line_num + 1

    if node:is_expanded() then
      for child in node:get_children_iter(depth) do
        render_node(child, depth + 1)
      end
    end
  end

  render_node(self.root, 0)

  api.nvim_buf_set_lines(self.buf, 0, -1, false, lines)
  api.nvim_buf_clear_namespace(self.buf, self._ns_id, 0, -1)

  for _, h in ipairs(highlights) do
    api.nvim_buf_add_highlight(
      self.buf,
      self._ns_id,
      h.hl,
      h.line,
      h.start,
      h["end"]
    )
  end

  for _, mark in ipairs(virt_line_marks) do
    api.nvim_buf_set_extmark(
      self.buf,
      self._ns_id,
      mark.line, -- 0-based line number
      0,         -- 0-based column
      {
        virt_lines = mark.lines,
        virt_lines_above = false,
      }
    )
  end
  api.nvim_set_option_value("modifiable", false, { buf = self.buf })
end

function Tree:node_by_line(linenr)
  return self._nodes_by_line[linenr]
end

function Tree:node_by_id(id)
  return self._nodes_by_id[id]
end

return Tree

local api = vim.api

---@class dm.ui.Help: dm.ui.Sidepanel.IComponent
local Help = {}

---@param groups dm.MappingsGroup[]
function Help.new(groups)
  ---@class dm.ui.Help
  local self = setmetatable({}, { __index = Help })
  self.buf = api.nvim_create_buf(false, true)
  self._hl_ns = api.nvim_create_namespace("HelpPopupHighlightNamespace")
  self.win = nil
  self.name = "[H]elp"
  local lines = {}
  ---@type {index: number, hlgroup: string}[]
  local highlights = {}

  for _, group in ipairs(groups) do
    if group.name then
      table.insert(highlights, {index = #lines, hlgroup = group.hlgroup})
      table.insert(lines, group.name)
    end
    for _, spec in ipairs(group.mappings) do
      if spec.desc then
        local key = spec.key
        local indent = string.rep(" ", 10 - #key)
        table.insert(lines, string.format("%s %s  %s", key, indent, spec.desc))
      end
    end
    table.insert(lines, "")
  end

  table.remove(lines)
  api.nvim_buf_set_lines(self.buf, 0, -1, false, lines)
  for _, hl in ipairs(highlights) do
    api.nvim_buf_add_highlight(self.buf, self._hl_ns, hl.hlgroup, hl.index, 0, -1)
  end
  api.nvim_set_option_value("modifiable", false, { buf = self.buf })
  return self
end

return Help

local widgets = require('dap.ui.widgets')
local api = vim.api

---@class dm.ui.Scopes: dm.ui.Sidepanel.IComponent
local Scopes = {}

function Scopes.new()
  ---@class dm.ui.Scopes
  local self = setmetatable({}, {__index = Scopes})
  local scopes = widgets.sidebar(widgets.scopes)
  local scopes_buf, scopes_win = scopes.open()
  api.nvim_win_close(scopes_win, true)
  self.buf = scopes_buf
  self.name = "[S]copes"
  vim.keymap.set("n", "<Tab>", "<CR>", {buffer = self.buf, remap = true})
  vim.keymap.del("n", "o", {buffer = self.buf })

  vim.keymap.set("n", "r", scopes.refresh, {buffer = self.buf})

  api.nvim_create_autocmd("User", {
    pattern = "DapSessionChanged",
    callback = vim.schedule_wrap(function()
      scopes.refresh()
    end)
  })

  return self
end

return Scopes

local M = {}
local keymaps = require("debugmaster.debug.keymaps")

M.cfg = require("debugmaster.cfg")

-- maybe want to lazyload it in the future. not critical right now anyway
M.plugins = require("debugmaster.plugins").plugins

M.mode = {
  toggle = function()
    require("debugmaster.plugins").init()
    require("debugmaster.state")
    require("debugmaster.debug.mode").toggle()
  end,
  disable = require("debugmaster.debug.mode").disable
}

M.keys = {
  get = keymaps.get,
  add = keymaps.add
}

return M

local repl = require 'dap.repl'
local dap = require("dap")
local api = vim.api

---@class dm.ui.Repl: dm.ui.Sidepanel.IComponent
local Repl = {}

function Repl.new()
  ---@class dm.ui.Repl
  local self = setmetatable({}, {__index = Repl})

  local repl_buf, repl_win = repl.open(nil, "vertical split")
  api.nvim_win_close(repl_win, true)
  self.name = "[R]epl"
  self.buf = repl_buf
  -- https://github.com/mfussenegger/nvim-dap/issues/786
  vim.keymap.set("i", "<C-w>", "<C-S-w>", {buffer = self.buf})
  vim.keymap.set("n", "<Tab>", "<CR>", {buffer = self.buf, remap = true})
  vim.keymap.del("n", "o", {buffer = self.buf })

  dap.listeners.after.initialize["repl-hl"] = function()
    pcall(vim.treesitter.stop, self.buf)
    pcall(vim.treesitter.start, self.buf, vim.o.filetype)
  end

  return self
end

return Repl

local groups = require("debugmaster.debug.keymaps").groups

---@class dm.State
local M = {}

M.sidepanel = require("debugmaster.ui.Sidepanel").new()
M.terminal = require("debugmaster.ui.Terminal").new()
M.repl = require("debugmaster.ui.Repl").new()
M.scopes = require("debugmaster.ui.Scopes").new()
M.help = require("debugmaster.ui.Help").new(groups)
M.breakpoints = require("debugmaster.ui.Breakpoints").new()

M.sidepanel:add_component(M.scopes)
M.sidepanel:add_component(M.repl)
M.sidepanel:add_component(M.terminal)
M.sidepanel:add_component(M.help)

M.sidepanel:set_active(M.scopes)

return M

---@class dm.Cfg
local cfg = {
  -- nothing here right now 😱
}

return cfg

This code segment functions to: Manage debugging breakpoints through operations including conditional creation, bulk deletion, and navigation between existing markers while integrating with debugging session controls.

```lua
[TASK_BEGIN]
---@type dm.MappingsGroup
local breakpoings_group = {
  name = "BREAKPOINTS",
  hlgroup = "DEBUG",
  mappings = {
    {
      key = "b",
      action = function()
        pcall(require("dap").toggle_breakpoint)
      end,
      desc = "Toggle breakpoint",
    },
    {
      key = "B",
      modes = { "n", "v" },
      action = function()
        local condition = nil
        if vim.fn.mode() == "v" then
          condition = utils.get_visual_selection()
        end
        pcall(require("dap").set_breakpoint, condition)
      end,
      desc = "Set conditional breakpoint (visual select for condition)",
    },
    {
      key = "L",
      modes = { "n", "v" },
      action = function()
        local log_message = nil
        if vim.fn.mode() == "v" then
          log_message = utils.get_visual_selection()
        end
        pcall(require("dap").set_breakpoint, nil, nil, log_message)
      end,
      desc = "Set log point (visual select for message)",
    },
    {
      key = "D",
      action = function()
        pcall(require("dap").clear_breakpoints)
      end,
      desc = "Clear all breakpoints",
    },
  }
}
[TASK_END]
```


local api = vim.api
local M = {}

local plugins = {}

---@class dm.Plugin
---@field activate fun() internal plugin function. User shouldn't call it
---@field enabled boolean? nil means plugin is enabled

plugins.cursor_hl = (function()
  ---@type dm.Plugin
  local plugin =

  return plugin
end)()

plugins.ui_auto_toggle = (function()
  ---@type dm.Plugin
  local plugin = {
    activate = function()
      local dap = require("dap")
      dap.listeners.before.launch["debugmaster"] = function()
        require("debugmaster.state").sidepanel:open()
      end

      dap.listeners.before.attach["debugmaster"] = function()
        require("debugmaster.state").sidepanel:open()
      end

      dap.listeners.before.event_terminated["debugmaster"] = function()
        require("debugmaster.state").sidepanel:close()
        print("dap terminated")
      end

      dap.listeners.before.event_exited["debugmaster"] = function()
        require("debugmaster.state").sidepanel:close()
        print("dap exited")
      end

      dap.listeners.before.disconnect["debugmaster"] = function()
        require("debugmaster.state").sidepanel:close()
        print("dap disconnected")
      end
    end
  }
  return plugin
end)()

-- probably this code doesn't fit in the plugins concept. Need to move it somewhere
plugins.last_config_rerunner = (function()
  local dap = require("dap")
  local session_configs = {}
  local last_config = nil
  ---@type dm.Plugin
  local plugin = {
    activate = function()
      dap.listeners.after.event_initialized["dm-saveconfig"] = function(session)
        local config = session.config
        last_config = config
        session_configs[session.id] = config
      end
    end,
    run_last_cached = function()
      local session = require("dap").session()
      if session then
        local config = assert(session_configs[session.id], "Active session exist, but config doesn't. Strange...")
        return dap.run(config)
      elseif last_config then
        return dap.run(last_config)
      end
      print("No configuration available to re-run")
    end
  }
  return plugin
end)()

---@type dm.Plugin
plugins.dap_float_close_on_q = {
  activate = function()
    api.nvim_command 'autocmd FileType dap-float nnoremap <buffer><silent> q <cmd>close!<CR>'
  end,
}

---@type dm.Plugin
plugins.osv_integration = {
  enabled = false,
  activate = function()
    local dap = require("dap")
    local instance_n = 1
    dap.adapters.debugmasterosv = (function()
      local id = "nvimdebug"
      local buf = -1
      dap.listeners.after.disconnect[id] = function()
        pcall(vim.api.nvim_buf_delete, buf, { force = true, unload = true })
      end
      local function find_module_path(name, no_suffix)
        local suffixes = { string.format("lua/%s.lua", name), string.format("lua/%s/init.lua", name) }
        for _, suf in ipairs(suffixes) do
          local path = vim.api.nvim_get_runtime_file(suf, false)[1]
          if path then
            path = vim.fs.normalize(path)
            return no_suffix and path:match("(.+)" .. suf) or path
          end
        end
      end
      return function(callback)
        -- we can't capture even integers for the function we dump
        -- hence using this json trick
        ---@param vars init-vars
        local init = function(vars)
          vim.opt.rtp:prepend(vars.dap_path)
          vim.opt.rtp:prepend(vars.osv_path)
          -- disable output because it significantly degrade performance
          require('osv').launch({ blocking = true, port = vars.port, output = false })
        end
        ---@class init-vars
        local vars = {
          port = math.random(49152, 65535),
          osv_path = assert(find_module_path("osv", true), "abort: one-small-step-for-vimkind not installed!!!"),
          dap_path = assert(find_module_path("dap", true), "abort: dap not installed!!!"),
        }
        -- https://gist.github.com/veechs/bc40f1f39b30cb1251825f031cd6d978
        local cmd = string.format(
          [[split | terminal nvim --cmd "lua loadstring( vim.base64.decode('%s') )( vim.json.decode(vim.base64.decode('%s')) )"]],
          vim.base64.encode(string.dump(init)), vim.base64.encode(vim.json.encode(vars))
        )
        vim.cmd(cmd)
        buf = vim.api.nvim_get_current_buf()
        api.nvim_buf_set_name(buf, "nvim-debug-" .. instance_n)
        instance_n = instance_n + 1
        vim.api.nvim_win_close(0, true)
        dap.listeners.after.initialize[id] = function()
          local state = require("debugmaster.state")
          state.terminal:attach_terminal_to_current_session(buf)
          state.sidepanel:set_active(state.terminal)
          dap.listeners.after.initialize[id] = nil
        end
        callback({ type = 'server', host = "127.0.0.1", port = vars.port })
      end
    end)()

    dap.configurations.lua = dap.configurations.lua or {}
    table.insert(dap.configurations.lua, {
      type = 'debugmasterosv',
      request = 'attach',
      name = "Debug neovim (lua). Provided by debugmaster.nvim",
    })
  end
}

local plugins_enabled = false
M.init = function()
  if not plugins_enabled then
    for _, plugin in pairs(M.plugins) do
      if plugin.enabled == nil or plugin.enabled then
        plugin.activate()
      end
    end
    plugins_enabled = true
  end
end

M.plugins = plugins

return M

{
    activate = function()
      local dcursor = api.nvim_get_hl(0, { name = "dCursor" })
      if next(dcursor) == nil then
        api.nvim_set_hl(0, "dCursor", { bg = "#2da84f" })
      end
      local cursor_mode_off = vim.o.guicursor
      local cursor_mode_on = cursor_mode_off .. ",a:dCursor"
      api.nvim_create_autocmd("User", {
        pattern = "DebugModeChanged",
        callback = function(args)
          if args.data.enabled then
            vim.go.guicursor = cursor_mode_on
          else
            vim.go.guicursor = cursor_mode_off
          end
        end
      })
    end
  }

local utils = require("debugmaster.utils")
local api = vim.api
local M = {}

---@class dm.KeySpec
---@field key string
---@field action fun()
---@field desc string?
---@field nowait boolean?
---@field group string?
---@field modes table | nil Table with modes like in vim.keymap.set. {"n"} by default

---@class dm.MappingsGroup
---@field name string? Group name. Not shown in HelpPopup if nil
---@field hlgroup string? Define name highlight color in HelpPopup
---@field mappings dm.KeySpec[]

---@type dm.MappingsGroup
local move_debugger_group = {
  name = "MOVE DEBUGGER",
  hlgroup = "ERROR",
  -- the main goal when developing keymaps for debug mode is to not override normal mode motions
  -- We want to our DEBUG mode be "constant". So we can freely move and can't edit text
  mappings = {
    {
      key = "o",
      action = function() require("dap").step_over() end,
      desc = "Step over. Works with count (Try to type '5o')",
    },
    {
      key = "m",
      action = function() require("dap").step_into() end,
      desc = "Step into (mine deeper)",
    },
    {
      key = "q",
      action = function() require("dap").step_out() end,
      desc = "Step out ([q]uit current stack frame)",
    },
    {
      key = "c",
      nowait = true,
      action = function() require("dap").continue() end,
      desc = "Continue or start debug session",
    },
    {
      key = "r",
      action = function() require("dap").run_to_cursor() end,
      desc = "Run to cursor",
    },
  }
}

---@type dm.MappingsGroup
local sidepanel = {
  name = "SIDEPANEL",
  hlgroup = "STRING",
  mappings = {
    {
      key = "u",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:toggle()
      end,
      desc = "Toggle ui",
    },
    {
      key = "U",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:toggle_layout()
      end,
      desc = "Toggle ui float mode",
    },
    {
      key = "S",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:set_active_with_open(state.scopes)
      end,
      desc = "Open scopes (global, local, etc variables)",
    },
    {
      key = "T",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:set_active_with_open(state.terminal)
      end,
      desc = "Open terminal",
    },
    {
      key = "R",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:set_active_with_open(state.repl)
      end,
      desc = "Open repl",
    },
    {
      key = "H",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:set_active_with_open(state.help)
      end,
      desc = "Open help",
    },
    {
      key = "}",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:rotate(1)
      end,
      desc = "Rotate sidenapel clockwise",
    },
    {
      key = "{",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:rotate(-1)
      end,
      desc = "Rotate sidenapel anticlockwise",
    },
    {
      key = "-",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:resize(-10)
      end,
      desc = "Decrease sidenapel size",
    },
    {
      key = "+",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:resize(10)
      end,
      desc = "Increase sidepanel size",
    }
  }
}

---@type dm.MappingsGroup
local float_widgets = {
  name = "FLOAT WIDGETS",
  hlgroup = "STATEMENT",
  mappings = {
    {
      key = "df",
      action = function()
        local widgets = require("dap.ui.widgets")
        pcall(widgets.cursor_float, widgets.frames)
        utils.register_to_close_on_leave(api.nvim_get_current_win())
      end,
      desc = "Frames widget"
    },
    {
      key = "dt",
      action = function()
        local widgets = require("dap.ui.widgets")
        pcall(widgets.cursor_float, widgets.threads)
        utils.register_to_close_on_leave(api.nvim_get_current_win())
      end,
      desc = "Threads widget"
    },
    {
      key = "ds",
      action = function()
        local widgets = require("dap.ui.widgets")
        local ok, sessions = pcall(widgets.cursor_float, widgets.sessions)
        if not ok then
          return
        end
        utils.register_to_close_on_leave(api.nvim_get_current_win())
        vim.keymap.set("n", "<CR>", vim.schedule_wrap(function()
          require('dap.ui').trigger_actions({ mode = 'first' })
          api.nvim_exec_autocmds("User", { pattern = "DapSessionChanged" })
          require("dap").focus_frame()
        end), { expr = true, buffer = sessions.buf })
      end,
      desc = "Debug sessions widget",
    },
    {
      key = "db",
      action = function()
        local state = require("debugmaster.state")
        utils.open_floating_window(state.breakpoints.buf, {
          min_width = 60,
          additional_height = 3,
        })
        utils.register_to_close_on_leave(api.nvim_get_current_win())
        vim.bo[state.breakpoints.buf].filetype = "dap-float"
      end,
      desc = "Breakpoints widget"
    },
    {
      key = "I",
      modes = { "n", "v" },
      action = function()
        pcall(require('dap.ui.widgets').hover)
        utils.register_to_close_on_leave(api.nvim_get_current_win())
      end,
      desc = "Inspect variable or visually selected expression",
    },
  }
}

---@type dm.MappingsGroup
local breakpoings_group = {
  name = "BREAKPOINTS",
  hlgroup = "Boolean",
  mappings = {
    {
      key = "t",
      action = function() require("dap").toggle_breakpoint() end,
      desc = "Toggle breakpoint",
    },
    {
      key = "da",
      action = function()
        require("dap").clear_breakpoints()
        print("All breakpoints removed")
      end,
      desc = "Delete all breakpoints",
    },
    {
      key = "dc",
      action = function()
        local condition = vim.fn.input({ prompt = "Enter breakpoing condition: " })
        if condition ~= "" then
          require("dap").toggle_breakpoint(condition)
        end
      end,
      desc = "Add conditional breakpoint",
    },
    {
      key = "[b",
      action = function() require("debugmaster.utils").gotoBreakpoint("prev") end,
      desc = "Go to previous breakpoint"
    },
    {
      key = "]b",
      action = function() require("debugmaster.utils").gotoBreakpoint("next") end,
      desc = "Go to next breakpoint"
    },
  },
}

---@type dm.MappingsGroup
local misc_group = {
  name = "MISCELANOUS",
  hlgroup = "TYPE",
  mappings = {
    {
      key = "dr",
      desc = "Restart the current session or rerun last if none",
      action = function()
        require("debugmaster.plugins").plugins.last_config_rerunner.run_last_cached()
      end
    },
    {
      key = "dn",
      action = function()
        vim.cmd("DapNew")
      end,
      desc = "Debug start new sessions",
    },
    {
      key = "dq",
      action = function()
        require("dap").terminate()
        local state = require("debugmaster.state")
        state.sidepanel:close()
      end,
      desc = "Quit debug"
    },
    {
      key = "[s",
      action = function() require("dap").down() end,
      desc = "Go to previous stack frame"
    },
    {
      key = "]s",
      action = function() require("dap").up() end,
      desc = "Go to next stack frame"
    },
    {
      key = "dj",
      action = function() require("dap").focus_frame() end,
      desc = "Jump to the current stack frame"
    },
    {
      key = "x",
      action = function()
        local state = require("debugmaster.state")
        local text = vim.fn.getreg('"')
        require("dap").repl.execute("\n" .. text)
        state.sidepanel:set_active_with_open(state.repl)
        api.nvim_buf_call(state.repl.buf, function()
          vim.cmd("normal G")
        end)
      end,
      desc = "Execute last yanked or deleted text in the repl",
    },
    {
      key = "dm",
      action = function()
        local state = require("debugmaster.state")
        local terminal = state.terminal
        local buf = api.nvim_get_current_buf()
        local is_term = api.nvim_get_option_value('buftype', { buf = buf }) == 'terminal'
        if not is_term then
          return print("Current buffer isn't terminal. Can't move to the Terminal section")
        end
        local ok = terminal:attach_terminal_to_current_session(buf)
        if ok then
          for _, win in ipairs(utils.get_windows_for_buffer(buf)) do
            api.nvim_win_close(win, true)
          end
          state.sidepanel:open()
          state.sidepanel:set_active(terminal)
        end
      end,
      desc = "Move current terminal to the 'Terminal' segment"
    },
  }
}

---@type dm.MappingsGroup[]
M.groups = {
  move_debugger_group,
  breakpoings_group,
  sidepanel,
  float_widgets,
  misc_group,
}

---Give the reference to the key entry so you can remap it to something else
---Throws an error if the key doesn't exist
---@return dm.KeySpec
function M.get(key)
  for _, group in ipairs(M.groups) do
    for _, mapping in ipairs(group.mappings) do
      if mapping.key == key then
        return mapping
      end
    end
  end
  error("Key doesn't exist")
end

--- Add new user mapping to the MISCELANOUS group
---@param mapping dm.KeySpec
function M.add(mapping)
  table.insert(misc_group.mappings, mapping)
end

return M

This code segment functions to: Configure a visual indicator that dynamically adjusts based on debugging state changes by establishing event listeners and modifying interface display settings when specific system events occur.

```lua
[TASK_BEGIN]
{
  activate = function()
    api.nvim_command 'autocmd CursorHold * lua vim.highlight.on_yank({on_visual = false})'
    api.nvim_command 'autocmd CursorHoldI * lua vim.highlight.on_yank({on_visual = false})'
  end
}
[TASK_END]
```


local utils = require("debugmaster.utils")
local api = vim.api
local M = {}

---@class dm.KeySpec
---@field key string
---@field action fun()
---@field desc string?
---@field nowait boolean?
---@field group string?
---@field modes table | nil Table with modes like in vim.keymap.set. {"n"} by default

---@class dm.MappingsGroup
---@field name string? Group name. Not shown in HelpPopup if nil
---@field hlgroup string? Define name highlight color in HelpPopup
---@field mappings dm.KeySpec[]

---@type dm.MappingsGroup
local move_debugger_group = {
  name = "MOVE DEBUGGER",
  hlgroup = "ERROR",
  -- the main goal when developing keymaps for debug mode is to not override normal mode motions
  -- We want to our DEBUG mode be "constant". So we can freely move and can't edit text
  mappings = {
    {
      key = "o",
      action = function() require("dap").step_over() end,
      desc = "Step over. Works with count (Try to type '5o')",
    },
    {
      key = "m",
      action = function() require("dap").step_into() end,
      desc = "Step into (mine deeper)",
    },
    {
      key = "q",
      action = function() require("dap").step_out() end,
      desc = "Step out ([q]uit current stack frame)",
    },
    {
      key = "c",
      nowait = true,
      action = function() require("dap").continue() end,
      desc = "Continue or start debug session",
    },
    {
      key = "r",
      action = function() require("dap").run_to_cursor() end,
      desc = "Run to cursor",
    },
  }
}

---@type dm.MappingsGroup
local sidepanel = {
  name = "SIDEPANEL",
  hlgroup = "STRING",
  mappings = {
    {
      key = "u",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:toggle()
      end,
      desc = "Toggle ui",
    },
    {
      key = "U",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:toggle_layout()
      end,
      desc = "Toggle ui float mode",
    },
    {
      key = "S",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:set_active_with_open(state.scopes)
      end,
      desc = "Open scopes (global, local, etc variables)",
    },
    {
      key = "T",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:set_active_with_open(state.terminal)
      end,
      desc = "Open terminal",
    },
    {
      key = "R",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:set_active_with_open(state.repl)
      end,
      desc = "Open repl",
    },
    {
      key = "H",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:set_active_with_open(state.help)
      end,
      desc = "Open help",
    },
    {
      key = "}",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:rotate(1)
      end,
      desc = "Rotate sidenapel clockwise",
    },
    {
      key = "{",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:rotate(-1)
      end,
      desc = "Rotate sidenapel anticlockwise",
    },
    {
      key = "-",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:resize(-10)
      end,
      desc = "Decrease sidenapel size",
    },
    {
      key = "+",
      action = function()
        local state = require("debugmaster.state")
        state.sidepanel:resize(10)
      end,
      desc = "Increase sidepanel size",
    }
  }
}

---@type dm.MappingsGroup
local float_widgets = {
  name = "FLOAT WIDGETS",
  hlgroup = "STATEMENT",
  mappings = {
    {
      key = "df",
      action = function()
        local widgets = require("dap.ui.widgets")
        pcall(widgets.cursor_float, widgets.frames)
        utils.register_to_close_on_leave(api.nvim_get_current_win())
      end,
      desc = "Frames widget"
    },
    {
      key = "dt",
      action = function()
        local widgets = require("dap.ui.widgets")
        pcall(widgets.cursor_float, widgets.threads)
        utils.register_to_close_on_leave(api.nvim_get_current_win())
      end,
      desc = "Threads widget"
    },
    {
      key = "ds",
      action = function()
        local widgets = require("dap.ui.widgets")
        local ok, sessions = pcall(widgets.cursor_float, widgets.sessions)
        if not ok then
          return
        end
        utils.register_to_close_on_leave(api.nvim_get_current_win())
        vim.keymap.set("n", "<CR>", vim.schedule_wrap(function()
          require('dap.ui').trigger_actions({ mode = 'first' })
          api.nvim_exec_autocmds("User", { pattern = "DapSessionChanged" })
          require("dap").focus_frame()
        end), { expr = true, buffer = sessions.buf })
      end,
      desc = "Debug sessions widget",
    },
    {
      key = "db",
      action = function()
        local state = require("debugmaster.state")
        utils.open_floating_window(state.breakpoints.buf, {
          min_width = 60,
          additional_height = 3,
        })
        utils.register_to_close_on_leave(api.nvim_get_current_win())
        vim.bo[state.breakpoints.buf].filetype = "dap-float"
      end,
      desc = "Breakpoints widget"
    },
    {
      key = "I",
      modes = { "n", "v" },
      action = function()
        pcall(require('dap.ui.widgets').hover)
        utils.register_to_close_on_leave(api.nvim_get_current_win())
      end,
      desc = "Inspect variable or visually selected expression",
    },
  }
}

---@type dm.MappingsGroup
local breakpoings_group = {
  name = "BREAKPOINTS",
  hlgroup = "Boolean",
  mappings = {
    {
      key = "t",
      action = function() require("dap").toggle_breakpoint() end,
      desc = "Toggle breakpoint",
    },
    {
      key = "da",
      action = function()
        require("dap").clear_breakpoints()
        print("All breakpoints removed")
      end,
      desc = "Delete all breakpoints",
    },
    {
      key = "dc",
      action = function()
        local condition = vim.fn.input({ prompt = "Enter breakpoing condition: " })
        if condition ~= "" then
          require("dap").toggle_breakpoint(condition)
        end
      end,
      desc = "Add conditional breakpoint",
    },
    {
      key = "[b",
      action = function() require("debugmaster.utils").gotoBreakpoint("prev") end,
      desc = "Go to previous breakpoint"
    },
    {
      key = "]b",
      action = function() require("debugmaster.utils").gotoBreakpoint("next") end,
      desc = "Go to next breakpoint"
    },
  },
}

---@type dm.MappingsGroup
local misc_group = {
  name = "MISCELANOUS",
  hlgroup = "TYPE",
  mappings =

}

---@type dm.MappingsGroup[]
M.groups = {
  move_debugger_group,
  breakpoings_group,
  sidepanel,
  float_widgets,
  misc_group,
}

---Give the reference to the key entry so you can remap it to something else
---Throws an error if the key doesn't exist
---@return dm.KeySpec
function M.get(key)
  for _, group in ipairs(M.groups) do
    for _, mapping in ipairs(group.mappings) do
      if mapping.key == key then
        return mapping
      end
    end
  end
  error("Key doesn't exist")
end

--- Add new user mapping to the MISCELANOUS group
---@param mapping dm.KeySpec
function M.add(mapping)
  table.insert(misc_group.mappings, mapping)
end

return M

{
    {
      key = "dr",
      desc = "Restart the current session or rerun last if none",
      action = function()
        require("debugmaster.plugins").plugins.last_config_rerunner.run_last_cached()
      end
    },
    {
      key = "dn",
      action = function()
        vim.cmd("DapNew")
      end,
      desc = "Debug start new sessions",
    },
    {
      key = "dq",
      action = function()
        require("dap").terminate()
        local state = require("debugmaster.state")
        state.sidepanel:close()
      end,
      desc = "Quit debug"
    },
    {
      key = "[s",
      action = function() require("dap").down() end,
      desc = "Go to previous stack frame"
    },
    {
      key = "]s",
      action = function() require("dap").up() end,
      desc = "Go to next stack frame"
    },
    {
      key = "dj",
      action = function() require("dap").focus_frame() end,
      desc = "Jump to the current stack frame"
    },
    {
      key = "x",
      action = function()
        local state = require("debugmaster.state")
        local text = vim.fn.getreg('"')
        require("dap").repl.execute("\n" .. text)
        state.sidepanel:set_active_with_open(state.repl)
        api.nvim_buf_call(state.repl.buf, function()
          vim.cmd("normal G")
        end)
      end,
      desc = "Execute last yanked or deleted text in the repl",
    },
    {
      key = "dm",
      action = function()
        local state = require("debugmaster.state")
        local terminal = state.terminal
        local buf = api.nvim_get_current_buf()
        local is_term = api.nvim_get_option_value('buftype', { buf = buf }) == 'terminal'
        if not is_term then
          return print("Current buffer isn't terminal. Can't move to the Terminal section")
        end
        local ok = terminal:attach_terminal_to_current_session(buf)
        if ok then
          for _, win in ipairs(utils.get_windows_for_buffer(buf)) do
            api.nvim_win_close(win, true)
          end
          state.sidepanel:open()
          state.sidepanel:set_active(terminal)
        end
      end,
      desc = "Move current terminal to the 'Terminal' segment"
    },
  }

This code segment functions to: Define additional debugger interaction commands including session management operations, stack frame navigation controls, and terminal integration handlers within a debugging interface component.

```lua
[TASK_BEGIN]
mappings = {
    {
      key = "Q",
      action = function()
        require("dap").close()
        require("dapui").close()
      end,
      desc = "Stop debugging and close all windows",
    },
    {
      key = "E",
      modes = { "n", "v" },
      action = function()
        require("dap.ui.widgets").hover()
        utils.register_to_close_on_leave(api.nvim_get_current_win())
      end,
      desc = "Evaluate expression under cursor",
    },
    {
      key = "gQ",
      action = function()
        require("dap").disconnect()
        require("dapui").close()
      end,
      desc = "Force disconnect debugger",
    },
    {
      key = "dl",
      action = function()
        require("dap").run_last()
      end,
      desc = "Run last debug configuration",
    },
    {
      key = "dv",
      action = function()
        require("dap").set_log_level("TRACE")
        vim.notify("Debug log level set to TRACE", vim.log.levels.INFO)
      end,
      desc = "Set verbose logging level",
    },
  }
[TASK_END]
```