diff --git a/README.md b/README.md index e077f55..d342be6 100644 --- a/README.md +++ b/README.md @@ -15,19 +15,28 @@ Heavily inspired by emacs' [xcscope.el](https://github.com/dkogan/xcscope.el). - Tries to mimic vim's builtin cscope functionality. - Provides user command, `:Cscope` which acts same as good old `:cscope`. - Short commands are supported. e.g. `:Cs f g main` -- No need to add cscope database (`:cscope add `), it is automatically picked from current directory or `db_file` option. - Keymaps can be disabled using `disable_maps` option. - Supports `cscope` and `gtags-cscope`. Use `cscope.exec` option to specify executable. - `:Cstag ` does `tags` search if no results are found in `cscope`. -- `:Cscope build` builds cscope db - - `vim.g.cscope_maps_statusline_indicator` can be used in statusline to indicate ongoing db build. -- `:Cscope db add ` add db file(s) to cscope search -- `:Cscope db rm ` remove db file(s) from cscope search - For `nvim < 0.9`, legacy cscope will be used. It will support keymaps. It won't have all the niceties of lua port. - Opens results in quickfix, **telescope**, or **fzf-lua**. - Has [which-key.nvim](https://github.com/folke/which-key.nvim) hints. - See [this section](#vim-gutentags) for `vim-gutentags`. +### Cscope DB + +- Statically provide table of db paths in config (`db_file`) OR add them at runtime using `:Cs db add ...` +- `:Cs db add ` add db file(s) to cscope search. +- `:Cs db rm ` remove db file(s) from cscope search. +- `:Cs db show` show all db connections. +- `:Cs db build` (re)builds db for primary db. +- `vim.g.cscope_maps_statusline_indicator` can be used in statusline to indicate ongoing db build. +- DB path grammar + - `db_file:db_pre_path` db_pre_path (prefix path) will be appended to cscope results. + - e.g. `:Cs db add ~/cscope.out:/home/code/proj2` => results from `~/cscope.out` will be prefixed with `/home/code/proj2/` + - `@` can be used to indicate that parent of `db_file` is `db_pre_path`. + - e.g. `:Cs db add ../proj2/cscope.out:@` => results from `../proj2/cscope.out` will be prefixed with `../proj2/` + ### Stack View - Visualize tree of caller functions and called functions. @@ -82,8 +91,6 @@ _cscope_maps_ comes with following defaults: -- when table of DBs is provided - -- first DB is "primary" and others are "secondary" -- primary DB is used for build and project_rooter - -- secondary DBs must be built with absolute paths - -- or paths relative to cwd. Otherwise JUMP will not work. -- cscope executable exec = "cscope", -- "cscope" or "gtags-cscope" -- choose your fav picker diff --git a/doc/cscope_maps.txt b/doc/cscope_maps.txt index 8ce9d61..e7eb9ae 100644 --- a/doc/cscope_maps.txt +++ b/doc/cscope_maps.txt @@ -1,4 +1,4 @@ -*cscope_maps.txt* For Neovim >= v0.10.0 Last change: 2024 June 01 +*cscope_maps.txt* For Neovim >= v0.10.0 Last change: 2024 July 06 ============================================================================== Table of Contents *cscope_maps-table-of-contents* @@ -31,20 +31,30 @@ CSCOPE ~ - Tries to mimic vim’s builtin cscope functionality. - Provides user command, `:Cscope` which acts same as good old `:cscope`. - Short commands are supported. e.g. `:Cs f g main` -- No need to add cscope database (`:cscope add `), it is automatically picked from current directory or `db_file` option. - Keymaps can be disabled using `disable_maps` option. - Supports `cscope` and `gtags-cscope`. Use `cscope.exec` option to specify executable. - `:Cstag ` does `tags` search if no results are found in `cscope`. -- `:Cscope build` builds cscope db - - `vim.g.cscope_maps_statusline_indicator` can be used in statusline to indicate ongoing db build. -- `:Cscope db add ` add db file(s) to cscope search -- `:Cscope db rm ` remove db file(s) from cscope search - For `nvim < 0.9`, legacy cscope will be used. It will support keymaps. It won’t have all the niceties of lua port. - Opens results in quickfix, **telescope**, or **fzf-lua**. - Has which-key.nvim hints. - See |cscope_maps-this-section| for `vim-gutentags`. +CSCOPE DB ~ + +- Statically provide table of db paths in config (`db_file`) OR add them at runtime using `:Cs db add ...` +- `:Cs db add ` add db file(s) to cscope search. +- `:Cs db rm ` remove db file(s) from cscope search. +- `:Cs db show` show all db connections. +- `:Cs db build` (re)builds db for primary db. +- `vim.g.cscope_maps_statusline_indicator` can be used in statusline to indicate ongoing db build. +- DB path grammar + - `db_file:db_pre_path` db_pre_path (prefix path) will be appended to cscope results. + - e.g. `:Cs db add ~/cscope.out:/home/code/proj2` => results from `~/cscope.out` will be prefixed with `/home/code/proj2/` + - `@` can be used to indicate that parent of `db_file` is `db_pre_path`. + - e.g. `:Cs db add ../proj2/cscope.out:@` => results from `../proj2/cscope.out` will be prefixed with `../proj2/` + + STACK VIEW ~ - Visualize tree of caller functions and called functions. @@ -102,8 +112,6 @@ _cscope_maps_ comes with following defaults: -- when table of DBs is provided - -- first DB is "primary" and others are "secondary" -- primary DB is used for build and project_rooter - -- secondary DBs must be built with absolute paths - -- or paths relative to cwd. Otherwise JUMP will not work. -- cscope executable exec = "cscope", -- "cscope" or "gtags-cscope" -- choose your fav picker diff --git a/lua/cscope/db.lua b/lua/cscope/db.lua new file mode 100644 index 0000000..3a1fd41 --- /dev/null +++ b/lua/cscope/db.lua @@ -0,0 +1,121 @@ +local utils = require("cscope_maps.utils") +local log = require("cscope_maps.utils.log") + +local M = {} + +--- conns = { {file = db_file, pre_path = db_pre_path}, ... } +M.conns = {} +M.global_conn = nil + +---Get all db connections +---If global connection is declared then use that +---@return table +M.all_conns = function() + M.update_global_conn() + return M.global_conn or M.conns +end + +---Get primary db connection +---If global connection is declared then use that +---@return table +M.primary_conn = function() + M.update_global_conn() + if M.global_conn then + return M.global_conn[1] + end + return M.conns[1] +end + +---Update primary db connection +---@param file string +---@param pre_path string +M.update_primary_conn = function(file, pre_path) + M.conns[1].file = vim.fs.normalize(file) + M.conns[1].pre_path = vim.fs.normalize(pre_path) +end + +---Update global db connection +M.update_global_conn = function() + if vim.g.cscope_maps_db_file then + local file, pre_path = M.sp_file_pre_path(vim.g.cscope_maps_db_file) + M.global_conn = { { file = file, pre_path = pre_path } } + else + M.global_conn = nil + end +end + +---Split input to ":Cs db add" into file and pre_path +---@param path string +---@return string +---@return string|nil +M.sp_file_pre_path = function(path) + local sp = vim.split(path, ":") + local file = vim.fs.normalize(sp[1]) + + ---@type string|nil + local pre_path = sp[2] + + -- use parent as pre_path if its "@" + if pre_path and pre_path == "@" then + pre_path = utils.get_path_parent(file) + end + + -- make it nil if its empty + if pre_path and pre_path == "" then + pre_path = nil + end + + -- if pre_path exists, normalize it + if pre_path then + pre_path = vim.fs.normalize(pre_path) + end + + return file, pre_path +end + +---Find index of db in all connections +---@param file string +---@param pre_path string|nil +---@return integer +M.find = function(file, pre_path) + for i, cons in ipairs(M.conns) do + if cons.file == file and ((pre_path and cons.pre_path == pre_path) or cons.pre_path == nil) then + return i + end + end + + return -1 +end + +---Add db in db connections +---@param path string +M.add = function(path) + local file, pre_path = M.sp_file_pre_path(path) + if M.find(file, pre_path) == -1 then + table.insert(M.conns, { file = file, pre_path = pre_path }) + end +end + +---Remove db from db connections +---Primary db connection will not be removed +---@param path string +M.remove = function(path) + local file, pre_path = M.sp_file_pre_path(path) + local loc = M.find(file, pre_path) + -- do not remove first entry + if loc > 1 then + table.remove(M.conns, loc) + end +end + +M.print_conns = function() + if not M.conns then + log.warn("No connections") + end + + for _, conn in ipairs(M.conns) do + log.warn(string.format("db=%s pre_path=%s", conn.file, conn.pre_path)) + end +end + +return M diff --git a/lua/cscope/init.lua b/lua/cscope/init.lua index 2383b45..5396b78 100644 --- a/lua/cscope/init.lua +++ b/lua/cscope/init.lua @@ -2,6 +2,7 @@ local RC = require("cscope_maps.utils.ret_codes") local log = require("cscope_maps.utils.log") local helper = require("cscope_maps.utils.helper") local utils = require("cscope_maps.utils") +local db = require("cscope.db") local M = {} @@ -54,7 +55,6 @@ for k, v in pairs(M.op_s_n) do end local cscope_picker = nil -local project_root = nil M.help = function() print([[ @@ -80,14 +80,6 @@ help : Show this message (Usage: help) ]]) end ---- if opts.db_file is a table then return 1st item -M.get_db_file = function() - if type(M.opts.db_file) == "table" then - return M.opts.db_file[1] - end - return M.opts.db_file -end - M.push_tagstack = function() local from = { vim.fn.bufnr("%"), vim.fn.line("."), vim.fn.col("."), 0 } local items = { { tagname = vim.fn.expand(""), from = from } } @@ -107,22 +99,17 @@ M.push_tagstack = function() vim.fn.settagstack(vim.fn.win_getid(), { items = items }, "t") end -M.parse_line = function(line) +M.parse_line = function(line, db_pre_path) local t = {} -- Populate t with filename, context and linenumber local sp = vim.split(line, "%s+") t.filename = sp[1] - if M.opts.picker ~= "telescope" then - -- workaround until https://github.com/nvim-telescope/telescope.nvim/pull/3151 is merged - t.filename = utils.get_rel_path(vim.fn.getcwd(), t.filename) - end - - -- update path if project_rooter is enabled - if M.opts.project_rooter.enable and not M.opts.project_rooter.change_cwd and project_root ~= nil then - t.filename = project_root .. "/" .. t.filename + if db_pre_path then + t.filename = vim.fs.joinpath(db_pre_path, t.filename) end + t.filename = utils.get_rel_path(vim.fn.getcwd(), t.filename) t.ctx = sp[2] t.lnum = sp[3] @@ -144,14 +131,14 @@ M.parse_line = function(line) return t end -M.parse_output = function(cs_out) +M.parse_output = function(cs_out, db_pre_path) -- Parse cscope output to be populated in QuickFix List -- setqflist() takes list of dicts to be shown in QF List. See :h setqflist() local res = {} for line in string.gmatch(cs_out, "([^\n]+)") do - local parsed_line = M.parse_line(line) + local parsed_line = M.parse_line(line, db_pre_path) table.insert(res, parsed_line) end @@ -191,21 +178,22 @@ end M.get_result = function(op_n, op_s, symbol, hide_log) -- Executes cscope search and return parsed output - local db_file = vim.g.cscope_maps_db_file or M.opts.db_file + local db_conns = db.all_conns() local cmd = string.format("%s -dL -%s %s", M.opts.exec, op_n, symbol) local out = "" + local any_res = false + local res = {} if M.opts.exec == "cscope" then - if type(db_file) == "string" then + for _, db_con in ipairs(db_conns) do + local db_file, db_pre_path = db_con.file, db_con.pre_path if vim.loop.fs_stat(db_file) ~= nil then - cmd = string.format("%s -f %s", cmd, db_file) - out = M.cmd_exec(cmd) - end - else -- table - for _, db in ipairs(db_file) do - if vim.loop.fs_stat(db) ~= nil then - local _cmd = string.format("%s -f %s", cmd, db) - out = string.format("%s%s", out, M.cmd_exec(_cmd)) + local _cmd = string.format("%s -f %s", cmd, db_file) + print(_cmd) + out = M.cmd_exec(_cmd) + if out ~= "" then + any_res = true + res = vim.tbl_deep_extend("keep", res, M.parse_output(out, db_pre_path)) end end end @@ -225,12 +213,12 @@ M.get_result = function(op_n, op_s, symbol, hide_log) return RC.INVALID_EXEC, nil end - if out == "" then + if any_res == false then log.warn("no results for 'cscope find " .. op_s .. " " .. symbol .. "'", hide_log) return RC.NO_RESULTS, nil end - return RC.SUCCESS, M.parse_output(out) + return RC.SUCCESS, res end M.find = function(op, symbol) @@ -293,8 +281,10 @@ M.db_build = function() local stdout = vim.loop.new_pipe(false) local stderr = vim.loop.new_pipe(false) local db_build_cmd_args = vim.tbl_deep_extend("force", M.opts.db_build_cmd_args, {}) - local cur_path = vim.fn.expand("%:p:h", true) - local db_file = M.get_db_file() + local cur_path = vim.fn.getcwd() + local db_conn = db.primary_conn() -- TODO: extend support to all db conns + local db_file = db_conn.file + local db_root = utils.get_path_parent(db_file) if vim.g.cscope_maps_statusline_indicator then log.warn("db build is already in progress") @@ -306,9 +296,7 @@ M.db_build = function() table.insert(db_build_cmd_args, db_file) end - if M.opts.project_rooter.enable and not M.opts.project_rooter.change_cwd and project_root ~= nil then - vim.cmd("cd " .. project_root) - end + vim.cmd("cd " .. db_root) local handle = nil vim.g.cscope_maps_statusline_indicator = M.opts.statusline_indicator or M.opts.exec @@ -330,9 +318,7 @@ M.db_build = function() log.warn("database build failed") end vim.g.cscope_maps_statusline_indicator = nil - if M.opts.project_rooter.enable and not M.opts.project_rooter.change_cwd and project_root ~= nil then - vim.cmd("cd " .. cur_path) - end + vim.cmd("cd " .. cur_path) end) ) vim.loop.read_start(stdout, M.db_build_output) @@ -340,30 +326,15 @@ M.db_build = function() end M.db_update = function(op, files) - if type(M.opts.db_file) == "string" then - M.opts.db_file = { M.opts.db_file } - end - if op == "a" then for _, f in ipairs(files) do - if not vim.tbl_contains(M.opts.db_file, f) then - table.insert(M.opts.db_file, f) - end + db.add(f) end elseif op == "r" then - local rm_keys = {} - for i, db in ipairs(M.opts.db_file) do - if vim.tbl_contains(files, db) then - table.insert(rm_keys, 1, i) - end - end - for _, v in ipairs(rm_keys) do - if v ~= 1 then - table.remove(M.opts.db_file, v) - end + for _, f in ipairs(files) do + db.remove(f) end end - log.warn("updateed DB list: " .. vim.inspect(M.opts.db_file)) end M.run = function(args) @@ -415,7 +386,7 @@ M.run = function(args) M.db_update(op, files) elseif op == "s" then - log.warn("current DB list: " .. vim.inspect(M.opts.db_file)) + db.print_conns() else log.warn("invalid operation") end @@ -474,31 +445,6 @@ M.user_command = function() }) end ---- returns parent dir where db_file is present -M.project_root = function(db_file) - local path = vim.fn.expand("%:p:h", true) - - while true do - if path == "" then - path = "/" - end - if vim.loop.fs_stat(path .. "/" .. db_file) ~= nil then - return path - end - if vim.fn.has("win32") then - if path:len() <= 2 then - return nil - end - path = path:match("^(.*)[/\\]") - else - if path == "/" then - return nil - end - path = path:match("^(.*)/") - end - end -end - ---Initialization API for inbuilt cscope ---Used for neovim < 0.9 M.legacy_setup = function() @@ -535,19 +481,26 @@ M.setup = function(opts) vim.g.cscope_maps_db_file = nil vim.g.cscope_maps_statusline_indicator = nil + if type(M.opts.db_file) == "string" then + db.add(M.opts.db_file) + else -- table + for _, f in ipairs(M.opts.db_file) do + db.add(f) + end + end + + -- if project rooter is enabled, + -- 1. get root of project and update primary conn + -- 2. if change_cwd is enabled, change into it (?) if M.opts.project_rooter.enable then - local db_file = M.get_db_file() - project_root = M.project_root(db_file) - if project_root ~= nil then - local new_db_path = string.format("%s/%s", project_root, db_file) - if type(M.opts.db_file) == "string" then - M.opts.db_file = new_db_path - else -- table - M.opts.db_file[1] = new_db_path - end + local primary_conn = db.primary_conn() + local root = vim.fs.root(0, primary_conn.file) + print("root" .. root) + if root then + db.update_primary_conn(vim.fs.joinpath(root, primary_conn.file), root) if M.opts.project_rooter.change_cwd then - vim.cmd("cd " .. project_root) + vim.cmd("cd " .. root) end end end diff --git a/lua/cscope/pickers/telescope.lua b/lua/cscope/pickers/telescope.lua index 7c4504a..8f19a8a 100644 --- a/lua/cscope/pickers/telescope.lua +++ b/lua/cscope/pickers/telescope.lua @@ -26,7 +26,7 @@ local entry_maker = function(entry) end end, ordinal = entry["filename"] .. entry["text"], - path = entry["filename"], + path = cs_utils.get_abs_path(entry["filename"]), lnum = tonumber(entry["lnum"]), } end diff --git a/lua/cscope_maps/utils/init.lua b/lua/cscope_maps/utils/init.lua index f1a312c..a91c683 100644 --- a/lua/cscope_maps/utils/init.lua +++ b/lua/cscope_maps/utils/init.lua @@ -1,7 +1,10 @@ local M = {} -local non_empty = function(item) - return item and item ~= "" +--- Check if given path is absolute path +---@param path string +---@return boolean +M.is_path_abs = function(path) + return vim.startswith(path, "/") end --- Get relative path @@ -11,13 +14,13 @@ end ---@param path string ---@return string M.get_rel_path = function(rel_to, path) - if not vim.startswith(rel_to, "/") or not vim.startswith(path, "/") then + if not M.is_path_abs(rel_to) or not M.is_path_abs(path) then return path end local rel_path = "" - local sp_rel_to = vim.tbl_filter(non_empty, vim.split(rel_to, "/")) - local sp_path = vim.tbl_filter(non_empty, vim.split(path, "/")) + local sp_rel_to = vim.split(vim.fs.normalize(rel_to), "/") + local sp_path = vim.split(vim.fs.normalize(path), "/") local len_rel_to = #sp_rel_to + 1 local len_path = #sp_path + 1 local i = 1 @@ -40,4 +43,27 @@ M.get_rel_path = function(rel_to, path) return rel_path end +--- Convert given path to absolute path +---@param path string +---@return string +M.get_abs_path = function(path) + if M.is_path_abs(path) then + return path + end + + local abs_path = vim.fs.joinpath(vim.fn.getcwd(), path) + + return vim.fs.normalize(abs_path) +end + +--- Get parent of given path +---@param path string +---@return string|nil +M.get_path_parent = function(path) + for parent in vim.fs.parents(path) do + return parent + end + return nil +end + return M