--============================================================================= -- todo.lua --- todo manager for SpaceVim -- Copyright (c) 2016-2023 Wang Shidong & Contributors -- Author: Wang Shidong < wsdjeg@outlook.com > -- URL: https://spacevim.org -- License: GPLv3 --============================================================================= local M = {} local sys = require('spacevim.api').import('system') local reg = require('spacevim.api').import('vim.regex') local project_manager_registered = false local bufnr = -1 local todo_jobid = -1 local todos = {} local todo = {} local winnr = -1 -- set the default labels for todo manager. local labels = { 'fixme', 'question', 'todo', 'idea' } local prefix = '@' local grep_default_exe, grep_default_opt, grep_default_ropt, grep_default_expr_opt, grep_default_fix_string_opt, grep_default_ignore_case, grep_default_smart_case = require('spacevim.plugin.search').default_tool() local logger = require('spacevim.logger').derive('todo') local jobstart = vim.fn.jobstart -- the labels_partten is used for viml function: matchstr etc local labels_partten = '' -- labels_regex is for command line argv local labels_regex = '' local function empty(d) -- {{{ if type(d) == 'string' then return d == '' end end -- }}} local function stderr(id, data, event) -- {{{ if id ~= todo_jobid then return end for _, d in ipairs(data) do logger.debug('stderr: ' .. d) end end -- }}} local function data_to_todo(d) -- {{{ logger.debug('stdout:' .. d) local f = vim.split(d, ':%d+:')[1] logger.debug('file is:' .. f) local i, j = string.find(d, ':%d+:') local line = string.sub(d, i + 1, j - 1) logger.debug('line is:' .. line) local column = string.sub(vim.fn.matchstr(d, [[\(:\d\+\)\@<=:\d\+:]]), 1, -2) local label = string.sub(vim.fn.matchstr(d, labels_partten), #prefix + 1, -1) logger.debug('label is:' .. label) local title = vim.fn.get(vim.fn.split(d, prefix .. label), 1, '') logger.debug('title is:' .. title) return { file = f, line = line, column = column, title = title, label = label, } end -- }}} local function stdout(id, data, event) -- {{{ if id ~= todo_jobid then return end for _, d in ipairs(data) do if not empty(d) then table.insert(todos, data_to_todo(d)) end end end -- }}} local function open_todo() -- {{{ local t = todos[vim.fn.line('.')] if vim.g.spacevim_todo_close_list == 1 or vim.g.spacevim_todo_close_list == true then vim.cmd('close') end vim.cmd(winnr .. 'wincmd w') vim.cmd('e ' .. t.file) vim.fn.cursor(t.line, t.column) vim.cmd('noautocmd normal! :') end -- }}} -- local function apply_to_quickfix() -- {{{ local qf = {} for _, v in ipairs(todos) do table.insert(qf, { filename = v.file, lnum = v.line, col = v.column, text = v.title, type = v.label, }) end vim.fn.setqflist({}, 'r', { title = 'Todos', items = qf, }) vim.cmd('close') vim.cmd(winnr .. 'wincmd w') vim.cmd('botright copen') end -- }}} local function indexof(t, v) -- {{{ for i, x in ipairs(t) do if x == v then return i end end return -1 end -- }}} local function extend(t1, t2) -- {{{ for _, v in ipairs(t2) do table.insert(t1, v) end end -- }}} local function compare_todo(a, b) -- {{{ local i = indexof(labels, a.label) local j = indexof(labels, b.label) if i < j then return true else return false end end -- }}} local function max_len(t, k) -- {{{ local l = 0 for _, v in ipairs(t) do l = math.max(#v[k], l) end return l end -- }}} local function on_exit(id, data, event) -- {{{ if id ~= todo_jobid then return end logger.info('todomanager job exit with:' .. data) table.sort(todos, compare_todo) local lw = max_len(todos, 'label') + 1 local fw = max_len(todos, 'file') + 1 local lines = {} for _, v in ipairs(todos) do table.insert( lines, prefix .. v.label .. string.rep(' ', lw - #v.label) .. v.file .. string.rep(' ', fw - #v.file) .. v.title ) end local ma = vim.fn.getbufvar(bufnr, '&ma') vim.fn.setbufvar(bufnr, '&ma', 1) -- update the buffer only when the buffer exists. if vim.fn.bufexists(bufnr) == 1 then vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) end vim.fn.setbufvar(bufnr, '&ma', ma) end -- }}} -- labels to command line regex local function get_labels_regex() -- {{{ local sep = '' if grep_default_exe == 'rg' then sep = '|' elseif grep_default_exe == 'grep' then sep = '\\|' else sep = '|' end local rst = '' local i = 1 for _, v in ipairs(labels) do rst = rst .. prefix .. v .. [[\b]] if i ~= #labels then rst = rst .. sep end i = i + 1 end return rst end -- }}} local function get_labels_partten() -- {{{ local sep = '|' local rst = [[\v]] local i = 1 local p = prefix if prefix == '@' then p = [[\@]] end for _, v in ipairs(labels) do rst = rst .. p .. v .. [[>]] if i ~= #labels then rst = rst .. sep end i = i + 1 end return rst end -- }}} local function update_todo_content() -- {{{ if vim.g.spacevim_todo_labels ~= nil then labels = vim.g.spacevim_todo_labels end if vim.g.spacevim_todo_prefix ~= nil then prefix = vim.g.spacevim_todo_prefix end todos = {} labels_regex = get_labels_regex() labels_partten = get_labels_partten() local argv = { grep_default_exe } extend(argv, grep_default_opt) extend(argv, { labels_regex }) if sys.isWindows and (grep_default_exe == 'rg' or grep_default_exe == 'ag' or grep_default_exe == 'pt') then extend(argv, { '.' }) elseif sys.isWindows and grep_default_exe == 'findstr' then extend(argv, { '*.*' }) elseif not sys.isWindows and grep_default_exe == 'rg' then extend(argv, { '.' }) end extend(argv, grep_default_ropt) logger.info('cmd:' .. vim.inspect(argv)) logger.info(' labels_partten: ' .. labels_partten) todo_jobid = jobstart(argv, { on_stdout = stdout, on_stderr = stderr, on_exit = on_exit, }) logger.info('jobid:' .. todo_jobid) end -- }}} local function open_win() -- {{{ if bufnr ~= 0 and vim.fn.bufexists(bufnr) == 1 then vim.cmd('bd ' .. bufnr) end vim.cmd('botright split __todo_manager__') winnr = vim.fn.winnr('#') local lines = vim.o.lines * 30 / 100 vim.cmd('resize ' .. lines) vim.cmd([[ setlocal buftype=nofile bufhidden=wipe nobuflisted nolist noswapfile nowrap cursorline nospell nonu norelativenumber winfixheight nomodifiable set filetype=SpaceVimTodoManager ]]) bufnr = vim.fn.bufnr('%') update_todo_content() vim.api.nvim_buf_set_keymap(bufnr, 'n', '', '', { callback = open_todo, }) vim.api.nvim_buf_set_keymap(bufnr, 'n', '', '', { callback = apply_to_quickfix, }) end -- }}} function M.list() -- {{{ if not project_manager_registered then require('spacevim.plugin.projectmanager').reg_callback(M.on_cwd_changed, 'update_todo_content') end open_win() end -- }}} function M.on_cwd_changed() if vim.api.nvim_buf_is_valid(bufnr) then update_todo_content() end end return M