diff --git a/README.md b/README.md index b2b61e4..4b90d05 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Bookmarks.nvim + > v1.1.0: line-highlight-and-db-backup > > Function Preview: https://lintao-index.pages.dev/docs/Vim/Neovim/my-plugins-docs/bookmarks.nvim/release-log @@ -36,16 +37,42 @@ return { Right now we have only one config options ```lua -return { "LintaoAmons/bookmarks.nvim", - config = function () - require("bookmarks").setup( { +return { + "LintaoAmons/bookmarks.nvim", + -- tag = "v0.5.4", -- optional, pin the plugin at specific version for stability + dependencies = { + { "nvim-telescope/telescope.nvim" }, + { "stevearc/dressing.nvim" }, -- optional: to have the same UI shown in the GIF + }, + config = function() + local opts = { + -- where you want to put your bookmarks db file (a simple readable json file, which you can edit manually as well, dont forget run `BookmarksReload` command to clean the cache) json_db_path = vim.fs.normalize(vim.fn.stdpath("config") .. "/bookmarks.db.json"), + -- This is how the sign looks. signs = { - mark = { icon = "󰃁", color = "red", line_bg= "#572626" }, + mark = { icon = "󰃁", color = "red", line_bg = "#572626" }, }, -- optional, backup the json db file when a new neovim session started and you try to mark a place -- you can find the file under the same folder enable_backup = true, + -- treeview options + treeview = { + bookmark_format = function(bookmark) + return bookmark.name .. " [" .. bookmark.location.project_name .. "] " .. bookmark.location.relative_path .. " : " .. bookmark.content + end, + keymap = { + quit = { "q", "" }, + refresh = "R", + create_folder = "a", + tree_cut = "x", + tree_paste = "p", + collapse = "o", + delete = "d", + active = "s", + copy = "c", + }, + }, + -- do whatever you like by hooks hooks = { { ---a sample hook that change the working directory when goto bookmark @@ -64,19 +91,22 @@ return { "LintaoAmons/bookmarks.nvim", end, }, }, - }) - end + } + require("bookmarks").setup(opts) + end, } ``` -## Commands and Keybindings +## Usage -There's two concepts in this plugin: `BookmarkList` and `Bookmark`. +There's two key concepts in this plugin: `BookmarkList` and `Bookmark`. You can look into the code to find the structure of those two domain objects +### Commands + | Command | Description | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `BookmarksMark` | Mark current line into active BookmarkList. Rename existing bookmark under cursor. Toggle it off if the new name is an empty string | @@ -85,6 +115,7 @@ You can look into the code to find the structure of those two domain objects | `BookmarksGotoRecent` | Go to latest visited/created Bookmark | | `BookmarksReload` | Clean the cache and resync the bookmarks jsonfile | | `BookmarksEditJsonFile` | An shortcut to edit bookmark jsonfile, remember BookmarksReload to clean the cache after you finish editing | +| `BookmarksTree` | Display all bookmarks with tree-view, and use "cut", "paste", "create folder" to edit the tree. |
BookmarksCommands(subcommands) we have right now @@ -125,6 +156,22 @@ vim.keymap.set("n", "ll", call_bookmark_command)
+### Bookmark Treeview + +
+BookmarksDisplay operations +a: add new folder +x: cut folder or bookmark +c: copy folder or bookmark +p: paste folder or bookmark +d: delete folder or bookmark +o: collapse or expand folder +s: active the current bookmark_list +q: quit +
+ +### Keymap + This plugin don't provide any default keybinding. I recommend you to have those three keybindings. ```lua @@ -172,7 +219,10 @@ By [telegram](https://t.me/+ssgpiHyY9580ZWFl) or [微信: CateFat](https://linta - [ ] Sequance diagram out of bookmarks: Pattern `[actor] -->actor sequance_number :: desc` - [ ] buffer renderer - [ ] filetree-like BookmarkList and Bookmark browsing. - - use `nui` or a `custom buffer` + - [x] MVP + - [ ] floating preview window + - [ ] better default bookmark render format(string format, then better UI) + - [ ] `u` undo. Expecially for unexpected `d` delete - custom buffer (can render more things, and can nav/copy/paste with ease) - local Keybindings - popup window diff --git a/lua/bookmarks/adapter/common.lua b/lua/bookmarks/adapter/common.lua index c5e1365..84ddae9 100644 --- a/lua/bookmarks/adapter/common.lua +++ b/lua/bookmarks/adapter/common.lua @@ -3,15 +3,10 @@ local utils = require("bookmarks.utils") ---@param bookmark Bookmarks.Bookmark ---@param bookmarks Bookmarks.Bookmark[] local function format(bookmark, bookmarks) - local max_len_listname = 0 local max_len_name = 0 local max_len_path = 0 for _, b in ipairs(bookmarks) do - if b.listname and #b.listname > max_len_listname then - max_len_listname = #b.listname - end - if #b.name > max_len_name then max_len_name = #b.name end @@ -23,8 +18,7 @@ local function format(bookmark, bookmarks) end return string.format( - "%-" .. max_len_listname .. "s %-" .. max_len_name .. "s %-" .. max_len_path .. "s", - bookmark.listname or "", + "%-" .. max_len_name .. "s %-" .. max_len_path .. "s", bookmark.name, bookmark.location.project_name .. "/" .. utils.shorten_file_path(bookmark.location.relative_path) ) diff --git a/lua/bookmarks/adapter/picker.lua b/lua/bookmarks/adapter/picker.lua index e250d7b..d2853cd 100644 --- a/lua/bookmarks/adapter/picker.lua +++ b/lua/bookmarks/adapter/picker.lua @@ -1,4 +1,6 @@ local repo = require("bookmarks.repo") +local _mark_repo = require("bookmarks.repo.bookmark") +local _bookmark_list = require("bookmarks.domain").bookmark_list local common = require("bookmarks.adapter.common") -- TODO: check dependencies firstly @@ -63,12 +65,12 @@ local function pick_bookmark(callback, opts) local bookmarks local bookmark_list_name if opts.all then - bookmarks = repo.mark.read.find_all() + bookmarks = _mark_repo.read.find_all() bookmark_list_name = "All" else local bookmark_list = opts.bookmark_list or repo.bookmark_list.write.find_or_set_active() bookmark_list_name = bookmark_list.name - bookmarks = bookmark_list.bookmarks + bookmarks = _bookmark_list.get_all_marks(bookmark_list) end table.sort(bookmarks, function(a, b) diff --git a/lua/bookmarks/api.lua b/lua/bookmarks/api.lua index ba763b8..21e5f27 100644 --- a/lua/bookmarks/api.lua +++ b/lua/bookmarks/api.lua @@ -49,7 +49,7 @@ local function add_list(param) end, bookmark_lists) ---@type Bookmarks.BookmarkList - local new_list = domain.bookmark_list.new(param.name, repo.generate_datetime_id()) + local new_list = domain.bookmark_list.new(param.name, utils.generate_datetime_id()) table.insert(new_lists, new_list) repo.bookmark_list.write.save_all(new_lists) @@ -93,6 +93,8 @@ local function goto_bookmark(bookmark, opts) for _, hook in ipairs(hooks) do hook(bookmark, projects) end + + sign.refresh_signs() end local function goto_last_visited_bookmark() @@ -108,6 +110,8 @@ local function goto_last_visited_bookmark() if last_bookmark then goto_bookmark(last_bookmark) end + + sign.refresh_signs() end -- TODO: trigger by `BufferEnter` Event @@ -171,11 +175,6 @@ local function open_bookmarks_jsonfile() vim.cmd("e " .. vim.g.bookmarks_config.json_db_path) end -local function buffer_display() - local lists = repo.bookmark_list.read.find_all() - require("bookmarks.render.main").render(lists) -end - return { mark = mark, rename_bookmark = rename_bookmark, @@ -194,5 +193,5 @@ return { open_bookmarks_jsonfile = open_bookmarks_jsonfile, }, - buffer_display = buffer_display, + tree = require("bookmarks.tree.api"), } diff --git a/lua/bookmarks/config.lua b/lua/bookmarks/config.lua index b024ba1..d8a9432 100644 --- a/lua/bookmarks/config.lua +++ b/lua/bookmarks/config.lua @@ -3,18 +3,59 @@ ---@field signs Signs ---@field hooks? Bookmarks.Hook[] local default_config = { + -- where you want to put your bookmarks db file (a simple readable json file, which you can edit manually as well, dont forget run `BookmarksReload` command to clean the cache) json_db_path = vim.fs.normalize(vim.fn.stdpath("config") .. "/bookmarks.db.json"), + -- This is how the sign looks. signs = { - mark = { icon = "󰃁", color = "grey", line_bg = "#572626" }, - -- annotation = { icon = "󰆉", color = "grey" }, -- TODO: + mark = { icon = "󰃁", color = "red", line_bg = "#572626" }, }, + -- optional, backup the json db file when a new neovim session started and you try to mark a place + -- you can find the file under the same folder enable_backup = true, + -- treeview options + treeview = { + bookmark_format = function(bookmark) + return bookmark.name + .. " [" + .. bookmark.location.project_name + .. "] " + .. bookmark.location.relative_path + .. " : " + .. bookmark.content + end, + keymap = { + quit = { "q", "" }, + refresh = "R", + create_folder = "a", + tree_cut = "x", + tree_paste = "p", + collapse = "o", + delete = "d", + active = "s", + copy = "c", + }, + }, -- do whatever you like by hooks - hooks = {}, + hooks = { + { + ---a sample hook that change the working directory when goto bookmark + ---@param bookmark Bookmarks.Bookmark + ---@param projects Bookmarks.Project[] + callback = function(bookmark, projects) + local project_path + for _, p in ipairs(projects) do + if p.name == bookmark.location.project_name then + project_path = p.path + end + end + if project_path then + vim.cmd("cd " .. project_path) + end + end, + }, + }, } -vim.g.bookmarks_config = default_config - ---@param user_config? Bookmarks.Config local setup = function(user_config) local cfg = vim.tbl_deep_extend("force", vim.g.bookmarks_config or default_config, user_config or {}) @@ -26,4 +67,5 @@ end return { setup = setup, + default_config = default_config, } diff --git a/lua/bookmarks/domain/bookmark.lua b/lua/bookmarks/domain/bookmark.lua index f0b93de..2e74640 100644 --- a/lua/bookmarks/domain/bookmark.lua +++ b/lua/bookmarks/domain/bookmark.lua @@ -7,7 +7,7 @@ local location_scope = require("bookmarks.domain.location") ---@field location Bookmarks.Location ---@field content string ---@field githash string ----@field listname? string +---@field listname? string -- TODO: remove this field, only used in repo when trying to find all marks, which is not reasonable ---@field created_at number ---@field visited_at number diff --git a/lua/bookmarks/domain/bookmark_list.lua b/lua/bookmarks/domain/bookmark_list.lua index d97fca2..6584947 100644 --- a/lua/bookmarks/domain/bookmark_list.lua +++ b/lua/bookmarks/domain/bookmark_list.lua @@ -1,11 +1,15 @@ local bookmark_scope = require("bookmarks.domain.bookmark") local utils = require("bookmarks.utils") +local _type = require("bookmarks.domain.type").type +local _get_value_type = require("bookmarks.domain.type").get_value_type ---@class Bookmarks.BookmarkList +---@field id string ---@field name string ---@field is_active boolean ----@field project_path_name_map {string: string} ----@field bookmarks Bookmarks.Bookmark[] +---@field project_path_name_map {string: string} -- bookmark specific path_name map, used to allow overwrite the project path to share with others +---@field bookmarks Bookmarks.Node[] +---@field collapse boolean treeview runtime status, may need refactor this field later local M = {} @@ -32,8 +36,12 @@ end function M.find_bookmark_by_location(self, location) -- TODO: self location path for _, b in ipairs(self.bookmarks) do - if b.location.path == location.path and b.location.line == location.line then - return b + local b_type = _get_value_type(b) + if b_type == _type.BOOKMARK then + ---@cast b Bookmarks.Bookmark + if b.location.path == location.path and b.location.line == location.line then + return b + end end end return nil @@ -88,4 +96,44 @@ function M.contains_bookmark(self, bookmark, projects) return false end +---@param self Bookmarks.BookmarkList +---@param id string | number +---@return Bookmarks.Bookmark? +function M.find_bookmark_by_id(self, id) + for _, b in ipairs(self.bookmarks) do + if b.id == id then + ---@type Bookmarks.Bookmark + return b + end + + if _get_value_type(b) == _type.BOOKMARK_LIST then + ---@cast b Bookmarks.BookmarkList + M.find_bookmark_by_id(b, id) + end + end + return nil +end + +---get all bookmarks in one dimension array +---@param self Bookmarks.BookmarkList +---@return Bookmarks.Bookmark[] +function M.get_all_marks(self) + local r = {} + + local function __get_all_marks(list, result) + for _, b in ipairs(list.bookmarks) do + if _get_value_type(b) == _type.BOOKMARK then + ---@cast b Bookmarks.Bookmark + table.insert(result, b) + else + __get_all_marks(b, result) + end + end + end + + __get_all_marks(self, r) + + return r +end + return M diff --git a/lua/bookmarks/domain/bookmark_node.lua b/lua/bookmarks/domain/bookmark_node.lua new file mode 100644 index 0000000..445c23b --- /dev/null +++ b/lua/bookmarks/domain/bookmark_node.lua @@ -0,0 +1,211 @@ +local _type = require("bookmarks.domain.type").type +local _get_value_type = require("bookmarks.domain.type").get_value_type +local utils = require("bookmarks.utils") +local bookmark_list = require("bookmarks.domain.bookmark_list") + +---@alias Bookmarks.Node (Bookmarks.Bookmark | Bookmarks.BookmarkList) + +local M = {} + +---@param root Bookmarks.Node +---@param target_id string | number +---@return Bookmarks.BookmarkList? +function M.get_father(root, target_id) + for _, child in ipairs(root.bookmarks) do + if child.id == target_id then + ---@type Bookmarks.BookmarkList + return root + end + + local cur_type = _get_value_type(child) + if cur_type ~= _type.BOOKMARK then + local ret = M.get_father(child, target_id) + if ret ~= nil then + return ret + end + end + end +end + +---@param father Bookmarks.BookmarkList +---@param brother Bookmarks.Node +---@param new_node Bookmarks.Node +function M.add_brother(father, brother, new_node) + for i, child in ipairs(father.bookmarks) do + if child.id == brother.id then + table.insert(father.bookmarks, i + 1, new_node) + return + end + end +end + +---@param self Bookmarks.BookmarkList +---@param id string | number +---@return Bookmarks.Node? +function M.remove_node(self, id) + for i, child in ipairs(self.bookmarks) do + if child.id == id then + return table.remove(self.bookmarks, i) + end + + local cur_type = _get_value_type(child) + if cur_type == _type.BOOKMARK_LIST then + ---@cast child Bookmarks.BookmarkList + local ret = M.remove_node(child, id) + if ret ~= nil then + return ret + end + end + end +end + +---@param self Bookmarks.BookmarkList +---@param id string | number +---@return Bookmarks.Node? +function M.copy_node(self, id) + local node = M.get_node(self, id) + if node == nil then + return nil + end + + return utils.deep_copy(node) +end + +---@param self Bookmarks.BookmarkList +---@param id string | number +---@param name string +function M.create_folder(self, id, name) + local cur_node = M.get_node(self, id) + if cur_node == nil then + utils.log("can't find current node") + return + end + + local folder_id = utils.generate_datetime_id() + local folder = bookmark_list.new(name, folder_id) + folder.is_active = false + local cur_type = _get_value_type(cur_node) + if cur_type == _type.BOOKMARK_LIST then + table.insert(cur_node.bookmarks, folder) + return + end + + local father = M.get_father(self, id) + if father == nil then + return + end + + -- TODO: add top level list + M.add_brother(father, cur_node, folder) +end + +---@param self Bookmarks.BookmarkList +---@param paste_id string | number +---@param node Bookmarks.Node +function M.paste(self, paste_id, node) + local cur_node = M.get_node(self, paste_id) + if cur_node == nil then + return + end + + local cur_type = _get_value_type(cur_node) + if cur_type == _type.BOOKMARK_LIST then + table.insert(cur_node.bookmarks, node) + return + end + + local cur_father = M.get_father(self, paste_id) + if cur_father == nil then + return + end + + M.add_brother(cur_father, cur_node, node) +end + +---@param root Bookmarks.Node +---@param target_id string | number +---@return Bookmarks.Node? +function M.get_node(root, target_id) + if root.id == target_id then + return root + end + + if root.bookmarks == nil then + return nil + end + + for _, b in ipairs(root.bookmarks) do + local ret = M.get_node(b, target_id) + if ret ~= nil then + return ret + end + end + + return nil +end + +---@param root Bookmarks.BookmarkList +---@param target_id string | number +---@return Bookmarks.Bookmark? +function M.collapse_node(root, target_id) + local cur_node = M.get_node(root, target_id) + if cur_node == nil then + return nil + end + + local cur_type = _get_value_type(cur_node) + if cur_type == _type.BOOKMARK then + ---@type Bookmarks.Bookmark + return cur_node + end + + ---@cast cur_node Bookmarks.BookmarkList + if cur_node.collapse then + cur_node.collapse = false + else + cur_node.collapse = true + end +end + +---@param father Bookmarks.BookmarkList +---@param son string | number +---@return boolean +function M.is_descendant(father, son) + for _, child in ipairs(father.bookmarks) do + if child.id == son then + return true + end + + local cur_type = _get_value_type(child) + if cur_type == _type.BOOKMARK_LIST then + ---@cast child Bookmarks.BookmarkList + local ret = M.is_descendant(child, son) + if ret then + return true + end + end + end + + return false +end + +---@param root Bookmarks.Node +---@param father string | number -- FIXME: why it could be a number? +---@param son string | number +---@return boolean +function M.is_descendant_by_id(root, father, son) + local father_node = M.get_node(root, father) + if father_node == nil then + return false + end + + local father_type = _get_value_type(father_node) + if father_type == _type.BOOKMARK then + return false + end + + ---@cast father_node Bookmarks.BookmarkList + return M.is_descendant(father_node, son) +end + +return M diff --git a/lua/bookmarks/domain/init.lua b/lua/bookmarks/domain/init.lua index e195e7d..a9883d4 100644 --- a/lua/bookmarks/domain/init.lua +++ b/lua/bookmarks/domain/init.lua @@ -1,7 +1,9 @@ local location = require("bookmarks.domain.location") local bookmark = require("bookmarks.domain.bookmark") local bookmark_list = require("bookmarks.domain.bookmark_list") +local bookmark_node = require("bookmarks.domain.bookmark_node") local project = require("bookmarks.domain.project") +local _type = require("bookmarks.domain.type") local BookmarkModule = { -- TODO: remove this method from domain module @@ -15,7 +17,9 @@ local BookmarkModule = { location = location, bookmark = bookmark, bookmark_list = bookmark_list, + node = bookmark_node, project = project, + type = _type, } return BookmarkModule diff --git a/lua/bookmarks/domain/type.lua b/lua/bookmarks/domain/type.lua new file mode 100644 index 0000000..ea7bc6f --- /dev/null +++ b/lua/bookmarks/domain/type.lua @@ -0,0 +1,19 @@ +local M = {} + +---@enum Bookmark.Type +M.type = { + BOOKMARK = 1, + BOOKMARK_LIST = 2, +} + +---@param val Bookmarks.Node +---@return Bookmark.Type +function M.get_value_type(val) + if val.bookmarks ~= nil then + return M.type.BOOKMARK_LIST + else + return M.type.BOOKMARK + end +end + +return M diff --git a/lua/bookmarks/render/main.lua b/lua/bookmarks/render/main.lua deleted file mode 100644 index 018fe1b..0000000 --- a/lua/bookmarks/render/main.lua +++ /dev/null @@ -1,67 +0,0 @@ -local M = {} - ----@param popup_content string[] ----@return {buf: integer, win: integer} -local function menu_popup_window(popup_content) - local popup_buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_lines(popup_buf, 0, -1, false, popup_content) - local width = vim.fn.strdisplaywidth(table.concat(popup_content, "\n")) - local height = #popup_content - - local opts = { - relative = "cursor", - row = 0, - col = 0, - width = width + 1, - height = height, - style = "minimal", - border = "single", - title = "ContextMenu.", - } - - local win = vim.api.nvim_open_win(popup_buf, true, opts) - return { - buf = popup_buf, - win = win, - } -end - ----@param bookmark_list Bookmarks.BookmarkList[] -function M.render(bookmark_lists) - local content = require("bookmarks.render.bookmarklist").render_lists(bookmark_lists) - local created = menu_popup_window(content) - - -- create local buffer shortcuts - vim.keymap.set({ "v", "n" }, "q", function() - vim.api.nvim_win_close(created.win, true) - end, { - noremap = true, - silent = true, - nowait = true, - buffer = created.buf, - }) - - -- - -- vim.keymap.set({ "v", "n" }, "", function() - -- quit_after_action(function() - -- local line = vim.api.nvim_get_current_line() - -- vim.print(line) - -- end, created.win) - -- end, { - -- noremap = true, - -- silent = true, - -- nowait = true, - -- buffer = created.buf, - -- }) - -- - -- vim.keymap.set({ "v", "n" }, "g?", function() - -- vim.print(" quit; trigger action under cursor") - -- end, { - -- noremap = true, - -- silent = true, - -- nowait = true, - -- buffer = created.buf, - -- }) -end - -return M diff --git a/lua/bookmarks/repo.lua b/lua/bookmarks/repo.lua index 87efe9d..29cfec6 100644 --- a/lua/bookmarks/repo.lua +++ b/lua/bookmarks/repo.lua @@ -1,3 +1,4 @@ +-- TODO: refactor to repo module local json = require("bookmarks.json") local utils = require("bookmarks.utils") @@ -47,23 +48,6 @@ local save_all = function(bookmark_lists) save_db(db) end --- Function to generate an ID in the datetime format -local function generate_datetime_id() - -- Get the current date and time in the desired format - local datetime = os.date("%Y%m%d%H%M%S") - - -- Generate a random number (e.g., using math.random) and append it to the datetime - local random_suffix = "" - for _ = 1, 8 do - random_suffix = random_suffix .. tostring(math.random(0, 9)) - end - - -- Concatenate the datetime and random suffix to create the ID - local id = datetime .. random_suffix - - return id -end - ---@return Bookmarks.BookmarkList[] local function find_all() return get_db().bookmark_lists or {} @@ -93,7 +77,7 @@ local function find_or_set_active_bookmark_list(bookmark_lists) if not active_bookmark_list then -- TODO: use domain logic to create new one active_bookmark_list = { - id = generate_datetime_id(), + id = utils.generate_datetime_id(), name = "Default", is_active = true, bookmarks = {}, @@ -142,7 +126,7 @@ local function get_recent_files_bookmark_list() return found[1] elseif #found == 0 then return { - id = generate_datetime_id(), + id = utils.generate_datetime_id(), name = name, is_active = false, bookmarks = {}, @@ -292,6 +276,4 @@ return { -- read get_recent_files_bookmark_list = get_recent_files_bookmark_list, - - generate_datetime_id = generate_datetime_id, } diff --git a/lua/bookmarks/repo/bookmark.lua b/lua/bookmarks/repo/bookmark.lua new file mode 100644 index 0000000..46ea3e5 --- /dev/null +++ b/lua/bookmarks/repo/bookmark.lua @@ -0,0 +1,27 @@ +local _repo = require("bookmarks.repo") +local _bookmark_list = require("bookmarks.domain").bookmark_list +local READ = {} +local WRITE = {} + +---@return Bookmarks.BookmarkList[] +local function find_all() + return _repo.db.get().bookmark_lists or {} +end + +function READ.find_all() + local bookmark_lists = find_all() + local all = {} + for _, bookmark_list in pairs(bookmark_lists) do + local bl = _bookmark_list.get_all_marks(bookmark_list) + for _, bookmark in ipairs(bl) do + bookmark.listname = bookmark_list.name + table.insert(all, bookmark) + end + end + return all +end + +return { + read = READ, + write = WRITE, +} diff --git a/lua/bookmarks/sign.lua b/lua/bookmarks/sign.lua index 5a02e3e..e3f31c7 100644 --- a/lua/bookmarks/sign.lua +++ b/lua/bookmarks/sign.lua @@ -1,4 +1,5 @@ local repo = require("bookmarks.repo") +local domain = require("bookmarks.domain") local ns_name = "BookmarksNvim" local hl_name = "BookmarksNvimSign" local hl_name_line = "BookmarksNvimLine" @@ -66,7 +67,9 @@ end local function _refresh_signs(bookmarks) clean() - bookmarks = bookmarks or repo.bookmark_list.write.find_or_set_active().bookmarks + local active_list = repo.bookmark_list.write.find_or_set_active() + + bookmarks = bookmarks or domain.bookmark_list.get_all_marks(active_list) local buf_number = vim.api.nvim_get_current_buf() for _, bookmark in ipairs(bookmarks) do local filepath = vim.fn.expand("%:p") @@ -84,7 +87,7 @@ end local function bookmark_sign_autocmd() -- TODO: check the autocmd vim.api.nvim_create_augroup(ns_name, { clear = true }) - vim.api.nvim_create_autocmd({ "BufWinEnter", "BufEnter" }, { + vim.api.nvim_create_autocmd({ "WinEnter", "WinLeave" }, { group = ns_name, callback = function(_) safe_refresh_signs() @@ -92,8 +95,36 @@ local function bookmark_sign_autocmd() }) end +local function clean_tree_cache(buf) + vim.b[buf]._bm_context = nil + vim.b[buf]._bm_tree_cut = nil +end + +local function refresh_tree() + local ctx = vim.g.bookmark_list_win_ctx + if ctx == nil then + return + end + + clean_tree_cache(ctx.buf) + + local bookmark_lists = repo.bookmark_list.read.find_all() + local context, lines = require("bookmarks.tree.render.context").from_bookmark_lists(bookmark_lists) + + vim.api.nvim_buf_set_option(ctx.buf, "modifiable", true) + vim.api.nvim_buf_set_lines(ctx.buf, 0, -1, false, lines) + vim.api.nvim_buf_set_option(ctx.buf, "modifiable", false) + + vim.b[ctx.buf]._bm_context = context +end + return { setup = setup, bookmark_sign_autocmd = bookmark_sign_autocmd, refresh_signs = safe_refresh_signs, + refresh_tree = refresh_tree, + namespace = { + ns = ns, + hl_name = hl_name, + }, } diff --git a/lua/bookmarks/tree/api.lua b/lua/bookmarks/tree/api.lua new file mode 100644 index 0000000..2fbb0e8 --- /dev/null +++ b/lua/bookmarks/tree/api.lua @@ -0,0 +1,136 @@ +local repo = require("bookmarks.repo") +local sign = require("bookmarks.sign") +local domain = require("bookmarks.domain") +local utils = require("bookmarks.utils") + +---@class Bookmarks.CopyContext +---@field line_no number +---@field opr string + +local M = {} + +---@param name string +---@param line_no number +function M.create_folder(name, line_no) + local ctx = vim.b._bm_context.line_contexts[line_no] + + local bookmark_list = repo.bookmark_list.read.must_find_by_name(ctx.root_name) + if not bookmark_list then + utils.log("No bookmark list find") + return + end + + domain.node.create_folder(bookmark_list, ctx.id, name) + + repo.bookmark_list.write.save(bookmark_list) + sign.refresh_tree() +end + +---@param line_no number +function M.cut(line_no) + vim.b._bm_tree_cut = { + line_no = line_no, + opr = "cut", + } + + local _namespace = require("bookmarks.sign").namespace + vim.api.nvim_buf_clear_namespace(0, _namespace.ns, 0, -1) + vim.api.nvim_buf_add_highlight(0, _namespace.ns, _namespace.hl_name, line_no - 1, 0, -1) +end + +---@param line_no number +function M.copy(line_no) + vim.b._bm_tree_cut = { + line_no = line_no, + opr = "copy", + } + + local _namespace = require("bookmarks.sign").namespace + vim.api.nvim_buf_clear_namespace(0, _namespace.ns, 0, -1) + vim.api.nvim_buf_add_highlight(0, _namespace.ns, _namespace.hl_name, line_no - 1, 0, -1) +end + +---@param line_no number +function M.paste(line_no) + ---@type Bookmarks.CopyContext + local opr_ctx = vim.b._bm_tree_cut + if not opr_ctx then + utils.log("No operation") + return + end + + ---@type Bookmarks.LineContext + local cut_ctx = vim.b._bm_context.line_contexts[opr_ctx.line_no] + local bookmark_list = repo.bookmark_list.read.must_find_by_name(cut_ctx.root_name) + if not bookmark_list then + utils.log("No bookmark list find") + return + end + + local ctx = vim.b._bm_context.line_contexts[line_no] + if domain.node.is_descendant_by_id(bookmark_list, cut_ctx.id, ctx.id) then + utils.log("Can't paste to descendant") + return + end + + local cut_node = nil + if opr_ctx.opr == "copy" then + cut_node = domain.node.copy_node(bookmark_list, cut_ctx.id) + else + cut_node = domain.node.remove_node(bookmark_list, cut_ctx.id) + end + + if not cut_node then + utils.log("No cut node") + return + end + + repo.bookmark_list.write.save(bookmark_list) + + local paste_bookmark_list = repo.bookmark_list.read.must_find_by_name(ctx.root_name) + if not paste_bookmark_list then + utils.log("No paste bookmark list") + return + end + + domain.node.paste(paste_bookmark_list, ctx.id, cut_node) + repo.bookmark_list.write.save(paste_bookmark_list) + + sign.refresh_tree() +end + +---@param line_no number +---@return Bookmarks.Bookmark? +function M.collapse(line_no) + ---@type Bookmarks.LineContext + local ctx = vim.b._bm_context.line_contexts[line_no] + local bookmark_list = repo.bookmark_list.read.must_find_by_name(ctx.root_name) + if not bookmark_list then + return + end + + local ret = domain.node.collapse_node(bookmark_list, ctx.id) + if ret then + return ret + end + + repo.bookmark_list.write.save(bookmark_list) + sign.refresh_tree() +end + +---@param line_no number +function M.delete(line_no) + local ctx = vim.b._bm_context.line_contexts[line_no] + local bookmark_list = repo.bookmark_list.read.must_find_by_name(ctx.root_name) + + domain.node.remove_node(bookmark_list, ctx.id) + repo.bookmark_list.write.save(bookmark_list) + + sign.refresh_tree() +end + +function M.open_treeview() + require("bookmarks.tree.operate").open_treeview() +end + +return M diff --git a/lua/bookmarks/tree/operate.lua b/lua/bookmarks/tree/operate.lua new file mode 100644 index 0000000..cfdf174 --- /dev/null +++ b/lua/bookmarks/tree/operate.lua @@ -0,0 +1,88 @@ +local api = require("bookmarks.api") +local sign = require("bookmarks.sign") +local repo = require("bookmarks.repo") + +local M = {} + +---@param name string +---@param line_no number +function M.create_folder_with_info(name, line_no) + api.tree.create_folder(name, line_no) +end + +function M.create_folder() + local line_no = vim.api.nvim_win_get_cursor(0)[1] + + vim.ui.input({ prompt = "add folder:", default = "" }, function(input) + if input then + M.create_folder_with_info(input, line_no) + end + end) +end + +function M.tree_cut() + local line_no = vim.api.nvim_win_get_cursor(0)[1] + api.tree.cut(line_no) +end + +function M.copy() + local line_no = vim.api.nvim_win_get_cursor(0)[1] + api.tree.copy(line_no) +end + +function M.tree_paste() + local line_no = vim.api.nvim_win_get_cursor(0)[1] + api.tree.paste(line_no) +end + +function M.collapse() + local line_no = vim.api.nvim_win_get_cursor(0)[1] + local bookmark = api.tree.collapse(line_no) + local ctx = vim.g.bookmark_list_win_ctx + if not ctx then + return + end + + if bookmark then + vim.api.nvim_set_current_win(ctx.previous_window) + require("bookmarks.api").goto_bookmark(bookmark) + end +end + +function M.delete() + local line_no = vim.api.nvim_win_get_cursor(0)[1] + api.tree.delete(line_no) +end + +function M.quit() + local ctx = vim.g.bookmark_list_win_ctx + vim.api.nvim_win_close(ctx.win, true) + vim.g.bookmark_list_win_ctx = nil +end + +function M.active() + local line_no = vim.api.nvim_win_get_cursor(0)[1] + local ctx = vim.b._bm_context.line_contexts[line_no] + api.set_active_list(ctx.root_name) + sign.refresh_tree() +end + +function M.open_treeview() + local ctx = vim.g.bookmark_list_win_ctx + + local win = (ctx ~= nil) and vim.api.nvim_win_is_valid(ctx.win) and ctx.win or nil + + local lists = repo.bookmark_list.read.find_all() + require("bookmarks.tree.render.main").render(lists, { win = win }) +end + +function M.refresh() + M.open_treeview() +end + +-- function M.open() +-- local line_no = vim.api.nvim_win_get_cursor(0)[1] +-- api.tree.open(line_no) +-- end + +return M diff --git a/lua/bookmarks/render/bookmark.lua b/lua/bookmarks/tree/render/bookmark.lua similarity index 70% rename from lua/bookmarks/render/bookmark.lua rename to lua/bookmarks/tree/render/bookmark.lua index c91db6f..a1121a3 100644 --- a/lua/bookmarks/render/bookmark.lua +++ b/lua/bookmarks/tree/render/bookmark.lua @@ -3,11 +3,13 @@ local M = {} ---@param bookmark Bookmarks.Bookmark ---@return string function M.render_bookmark(bookmark) - return " [" + if vim.g.bookmarks_config.treeview and vim.g.bookmarks_config.treeview.bookmark_formt then + return vim.g.bookmarks_config.treeview.bookmark_format(bookmark) + end + return bookmark.name + .. " [" .. bookmark.location.project_name .. "] " - .. bookmark.name - .. ": " .. bookmark.location.relative_path .. " : " .. bookmark.content diff --git a/lua/bookmarks/render/bookmarklist.lua b/lua/bookmarks/tree/render/bookmarklist.lua similarity index 100% rename from lua/bookmarks/render/bookmarklist.lua rename to lua/bookmarks/tree/render/bookmarklist.lua diff --git a/lua/bookmarks/tree/render/context.lua b/lua/bookmarks/tree/render/context.lua new file mode 100644 index 0000000..167d17e --- /dev/null +++ b/lua/bookmarks/tree/render/context.lua @@ -0,0 +1,88 @@ +local render_bookmark = require("bookmarks.tree.render.bookmark") +local _type = require("bookmarks.domain.type").type +local _get_value_type = require("bookmarks.domain.type").get_value_type + +local INTENT = " " +local M = {} + +---@class Bookmarks.LineContext +---@field deep number +---@field id number +---@field root_name string + +---@class Bookmarks.TreeContext +---@field line_contexts Bookmarks.LineContext[] + +---@param node Bookmarks.BookmarkList | Bookmarks.Bookmark +---@param deep number +---@return string +function M.render_context(node, deep) + local node_type = _get_value_type(node) + local icon = node.collapse and "▸" or "▾" + local book_icon = "" + + if node_type == _type.BOOKMARK then + ---@cast node Bookmarks.Bookmark + return string.rep(INTENT, deep) .. book_icon .. " " .. render_bookmark.render_bookmark(node) + else + local suffix = node.is_active and " *" or "" + return string.rep(INTENT, deep) .. icon .. node.name .. suffix + end +end + +---@param node Bookmarks.Node +---@param lines string[] +---@param line_contexts Bookmarks.LineContext[] +---@param deep number +function M.render_tree_recursive(node, lines, line_contexts, deep, root_id) + local ctx = M.to_line_context(node.id, deep, root_id) + local line = M.render_context(node, deep) + table.insert(lines, line) + table.insert(line_contexts, ctx) + + if node.collapse then + return + end + + if _get_value_type(node) == _type.BOOKMARK then + return + end + + for _, child in ipairs(node.bookmarks) do + M.render_tree_recursive(child, lines, line_contexts, deep + 1, root_id) + end +end + +---@param bookmark_lists Bookmarks.BookmarkList[] +---@return Bookmarks.TreeContext, string[] +function M.from_bookmark_lists(bookmark_lists) + local lines = {} + local line_contexts = {} + + table.sort(bookmark_lists, function(a, b) + return a.name < b.name + end) + + for _, bookmark_list in ipairs(bookmark_lists) do + M.render_tree_recursive(bookmark_list, lines, line_contexts, 0, bookmark_list.name) + end + + local ctx = { + line_contexts = line_contexts, + } + + return ctx, lines +end + +---@param id string | number +---@param deep number +---@return Bookmarks.LineContext +function M.to_line_context(id, deep, root_name) + return { + id = id, + deep = deep, + root_name = root_name, + } +end + +return M diff --git a/lua/bookmarks/tree/render/main.lua b/lua/bookmarks/tree/render/main.lua new file mode 100644 index 0000000..119de6f --- /dev/null +++ b/lua/bookmarks/tree/render/main.lua @@ -0,0 +1,73 @@ +local render_context = require("bookmarks.tree.render.context") +local tree_operate = require("bookmarks.tree.operate") +local config = require("bookmarks.config") + +local M = {} + +---@class Bookmarks.PopupWindowCtx +---@field buf integer +---@field win integer +---@field previous_window integer + +---@param opts {width: integer} +---@return integer +local function create_vsplit_with_width(opts) + vim.cmd("vsplit") + + local new_win = vim.api.nvim_get_current_win() + + vim.api.nvim_win_set_width(new_win, opts.width) + + return new_win +end + +local function register_local_shortcuts(buf) + local keymap = config.default_config.treeview.keymap + if vim.g.bookmarks_config.treeview and vim.g.bookmarks_config.treeview.keymap then + keymap = vim.g.bookmarks_config.treeview.keymap + end + + local options = { + noremap = true, + silent = true, + nowait = true, + buffer = buf, + } + + for action, keys in pairs(keymap) do + if type(keys) == "string" then + pcall(vim.keymap.set, { "v", "n" }, keys, tree_operate[action], options) + elseif type(keys) == "table" then + for _, k in ipairs(keys) do + pcall(vim.keymap.set, k, tree_operate[action], options) + end + end + end +end + +---@param bookmark_lists Bookmarks.BookmarkList[] +---@param opts {win: integer?, buf: integer?} +function M.render(bookmark_lists, opts) + opts = opts or {} + local cur_window = vim.api.nvim_get_current_win() + local context, lines = render_context.from_bookmark_lists(bookmark_lists) + + local buf = opts.buf or vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + vim.api.nvim_buf_set_option(buf, "modifiable", false) + + local win = opts.win or create_vsplit_with_width({ width = 30 }) + vim.api.nvim_win_set_buf(win, buf) + + vim.b[buf]._bm_context = context + + register_local_shortcuts(buf) + + vim.g.bookmark_list_win_ctx = { + buf = buf, + win = win, + previous_window = cur_window, + } +end + +return M diff --git a/lua/bookmarks/utils.lua b/lua/bookmarks/utils.lua index fa8d585..6c2a801 100644 --- a/lua/bookmarks/utils.lua +++ b/lua/bookmarks/utils.lua @@ -99,6 +99,24 @@ local function get_buf_relative_path() return string.sub(buf_path, string.len(project_path) + 2, string.len(buf_path)) end +-- Function to generate an ID in the datetime format +---@return string +local function generate_datetime_id() + -- Get the current date and time in the desired format + local datetime = os.date("%Y%m%d%H%M%S") + + -- Generate a random number (e.g., using math.random) and append it to the datetime + local random_suffix = "" + for _ = 1, 8 do + random_suffix = random_suffix .. tostring(math.random(0, 9)) + end + + -- Concatenate the datetime and random suffix to create the ID + local id = datetime .. random_suffix + + return id +end + return { trim = trim, shorten_file_path = shorten_file_path, @@ -108,4 +126,5 @@ return { find_project_name = find_project_name, get_buf_relative_path = get_buf_relative_path, log = log, + generate_datetime_id = generate_datetime_id, } diff --git a/plugin/bookmarks.lua b/plugin/bookmarks.lua index 1824ba5..f205317 100644 --- a/plugin/bookmarks.lua +++ b/plugin/bookmarks.lua @@ -9,6 +9,11 @@ if vim.g.loaded_bookmarks == 1 then end vim.g.loaded_bookmarks = 1 +-- all global variable should firstly declare at this place +vim.g.bookmarks_config = require("bookmarks.config").default_config +---@type Bookmarks.PopupWindowCtx +vim.g.bookmark_list_win_ctx = nil + require("bookmarks").setup() require("bookmarks.sign").bookmark_sign_autocmd() local adapter = require("bookmarks.adapter") @@ -40,4 +45,4 @@ vim.api.nvim_create_user_command( vim.api.nvim_create_user_command("BookmarksEditJsonFile", api.helper.open_bookmarks_jsonfile, { desc = "An shortcut to edit bookmark jsonfile, remember BookmarksReload to clean the cache after you finish editing", }) -vim.api.nvim_create_user_command("BookmarksDisplay", api.buffer_display, {}) +vim.api.nvim_create_user_command("BookmarksTree", api.tree.open_treeview, {})