Skip to content

feature: Search for todos from project root ( .obsidian , .git) #230

Description

@VigneshR387

Did you check the docs?

  • I have read all the docs

Is your feature request related to a problem? Please describe.

The current select_todo only fetches todos from the current buffer, but I think it can be made more robust by searching todos throughout the project root.

Describe the solution you'd like

A picker that can fetch to-dos throughout the project root.

Features:

  • User can set custom roots like .obsidian etc.
  • User can exclude directories or files from being searched by the picker. Useful when you have template files with checklist item in them.
  • User can invoke picker through keymap for complete and incomplete tasks. Can be extended for custom toggle_states.

Optional:

  • Ability to toggle tasks directly from the picker (may not be feasible depending on the picker's capabilities)

Describe alternatives you've considered

Using Snacks.picker.grep() to achieve something similar, but I think it can be better integrated with the plugin.

Searching incomplete todo:

{
      '<leader>Ti',
      function()
        local root = Util.root.get()
        local exclude_dirs = {
          'Templates',
          'node_modules',
          '.git',
          'dist',
          'build',
        }
        local exclude_patterns = vim.tbl_map(function(dir) return '**/' .. dir .. '/**' end, exclude_dirs)
        Snacks.picker.grep {
          prompt = '',
          search = '^\\s*- \\[ \\]',
          regex = true,
          -- no “live grep” needed here since we have a fixed pattern
          live = false,
          -- restrict search to the current working directory
          dirs = { root },
          -- Only show for markdown files
          glob = '*.md',
          exclude = exclude_patterns,
          -- include files ignored by .gitignore
          args = { '--no-ignore' },
          -- Start in normal mode
          on_show = function() vim.cmd.stopinsert() end,
          finder = 'grep',
          format = 'file',
          show_empty = true,
          supports_live = false,
          layout = 'ivy_split',
        }
      end,
      desc = 'Search for incomplete tasks',
    },


Searching completed todo:

    {
      '<leader>Tc',
      function()
        local root = Util.root.get()
        local exclude_dirs = {
          'Templates',
          'node_modules',
          '.git',
          'dist',
          'build',
        }
        local exclude_patterns = vim.tbl_map(function(dir) return '**/' .. dir .. '/**' end, exclude_dirs)
        Snacks.picker.grep {
          prompt = '',
          -- search = "^\\s*- \\[x\\] `done:",
          search = '^\\s*- \\[[xX]\\](?: `done:)?',
          regex = true,
          -- no “live grep” needed here since we have a fixed pattern
          live = false,
          -- restrict search to the current working directory
          dirs = { root },
          -- exclude directories
          exclude = exclude_patterns,
          -- include files ignored by .gitignore
          args = { '--no-ignore' },
          -- Start in normal mode
          on_show = function() vim.cmd.stopinsert() end,
          finder = 'grep',
          format = 'file',
          show_empty = true,
          supports_live = false,
          layout = 'ivy',
        }
      end,
      desc = 'Search for complete tasks',
    },
  },

lua/util/root.lua:

local  M = {}

function M.detect(opts)
  opts = opts or {}
  opts.spec = opts.spec or type(vim.g.root_spec) == "table" and vim.g.root_spec or M.spec
  opts.buf = (opts.buf == nil or opts.buf == 0) and vim.api.nvim_get_current_buf() or opts.buf

  local ret = {} ---@type UtilRoot[]
  for _, spec in ipairs(opts.spec) do
    local paths = M.resolve(spec)(opts.buf)
    paths = paths or {}
    paths = type(paths) == "table" and paths or { paths }
    local roots = {} ---@type string[]
    for _, p in ipairs(paths) do
      local pp = M.realpath(p)
      if pp and not vim.tbl_contains(roots, pp) then
        roots[#roots + 1] = pp
      end
    end
    table.sort(roots, function(a, b)
      return #a > #b
    end)
    if #roots > 0 then
      ret[#ret + 1] = { spec = spec, paths = roots }
      if opts.all == false then
        break
      end
    end
  end
  return ret
end

M.cache = {}

function M.is_win() return vim.uv.os_uname().sysname:find 'Windows' ~= nil end


function M.get(opts)
  opts = opts or {}
  local buf = opts.buf or vim.api.nvim_get_current_buf()
  local ret = M.cache[buf]
  if not ret then
    local roots = M.detect({ all = false, buf = buf })
    ret = roots[1] and roots[1].paths[1] or vim.uv.cwd()
    M.cache[buf] = ret
  end
  if opts and opts.normalize then
    return ret
  end
  return Util.is_win() and ret:gsub("/", "\\") or ret
end

return M

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions