From 092e7c8b2139fffa14b00e9aecbae277b59c0aa8 Mon Sep 17 00:00:00 2001 From: champignoom Date: Thu, 21 Dec 2023 21:42:34 +0800 Subject: [PATCH] feat(toc): support one ToC per tabpage --- lua/neorg/modules/core/qol/toc/module.lua | 230 +++++++++++----------- 1 file changed, 113 insertions(+), 117 deletions(-) diff --git a/lua/neorg/modules/core/qol/toc/module.lua b/lua/neorg/modules/core/qol/toc/module.lua index 9a83efa17..a7f5f845f 100644 --- a/lua/neorg/modules/core/qol/toc/module.lua +++ b/lua/neorg/modules/core/qol/toc/module.lua @@ -4,7 +4,6 @@ description: The TOC module geneates a table of contents for a given Norg buffer. summary: Generates a table of contents for a given Norg buffer. --- - The TOC module exposes a single command - `:Neorg toc`. This command can be executed with one of three optional arguments: `left`, `right` and `qflist`. @@ -63,8 +62,8 @@ module.config.public = { sync_cursorline = true, } -local data_of_ui_buf = {} -local data_of_norg_win = {} +local ui_data_of_tabpage = {} +local data_of_norg_buf = {} local toc_namespace local function upper_bound(array, v) @@ -85,19 +84,17 @@ local function upper_bound(array, v) return l end -local function get_target_location_under_cursor(ui_buffer, original_buffer) - local ui_window = vim.fn.bufwinid(ui_buffer) - local original_window = vim.fn.bufwinid(original_buffer) - +local function get_target_location_under_cursor(ui_data) + local ui_window = vim.fn.bufwinid(ui_data.buffer) local curline = vim.api.nvim_win_get_cursor(ui_window)[1] - local offset = data_of_ui_buf[ui_buffer].start_lines.offset - local extmark_lookup = data_of_norg_win[original_window].extmarks[curline - offset] + local offset = ui_data.start_lines.offset + local extmark_lookup = data_of_norg_buf[ui_data.norg_buffer].extmarks[curline - offset] if not extmark_lookup then return end - return vim.api.nvim_buf_get_extmark_by_id(original_buffer, toc_namespace, extmark_lookup, {}) + return vim.api.nvim_buf_get_extmark_by_id(ui_data.norg_buffer, toc_namespace, extmark_lookup, {}) end module.public = { @@ -187,17 +184,20 @@ module.public = { return qflist_data end, - update_cursor = function(original_buffer, ui_buffer) - local original_window = vim.fn.bufwinid(original_buffer) - local ui_window = vim.fn.bufwinid(ui_buffer) + -- Update ui cursor according to norg cursor + update_cursor = function(ui_data) + local norg_window = vim.fn.bufwinid(ui_data.norg_buffer) + local norg_data = data_of_norg_buf[ui_data.norg_buffer] + local ui_window = vim.fn.bufwinid(ui_data.buffer) + assert(ui_window ~= -1) - local current_row_1b = vim.fn.line(".", original_window) - if data_of_norg_win[original_window].last_row == current_row_1b then + local current_row_1b = vim.fn.line(".", norg_window) + if norg_data.last_row == current_row_1b then return end - data_of_norg_win[original_window].last_row = current_row_1b + norg_data.last_row = current_row_1b - local start_lines = data_of_ui_buf[ui_buffer].start_lines + local start_lines = ui_data.start_lines assert(start_lines) local current_toc_item_idx = upper_bound(start_lines, current_row_1b - 1) - 1 @@ -208,25 +208,28 @@ module.public = { vim.api.nvim_win_set_cursor(ui_window, { current_toc_row, 0 }) end, - update_toc = function(toc_title, original_buffer, original_window, ui_buffer, ui_window) + update_toc = function(toc_title, ui_data, norg_buffer) + local norg_window = vim.fn.bufwinid(norg_buffer) + local ui_buffer = ui_data.buffer + local ui_window = vim.fn.bufwinid(ui_buffer) + ui_data.norg_buffer = norg_buffer + vim.bo[ui_buffer].modifiable = true - vim.api.nvim_buf_clear_namespace(original_buffer, toc_namespace, 0, -1) + vim.api.nvim_buf_clear_namespace(norg_buffer, toc_namespace, 0, -1) table.insert(toc_title, "") vim.api.nvim_buf_set_lines(ui_buffer, 0, -1, true, toc_title) - local ui_buf_data = {} - data_of_ui_buf[ui_buffer] = ui_buf_data - local norg_win_data = {} - data_of_norg_win[original_window] = norg_win_data + local norg_data = {} + data_of_norg_buf[norg_buffer] = norg_data local prefix, title local extmarks = {} - norg_win_data.extmarks = extmarks + norg_data.extmarks = extmarks local offset = vim.api.nvim_buf_line_count(ui_buffer) local start_lines = { offset = offset } - ui_buf_data.start_lines = start_lines + ui_data.start_lines = start_lines local toc_filter = module.config.public.toc_filter @@ -262,15 +265,15 @@ module.public = { table.insert( extmarks, - vim.api.nvim_buf_set_extmark(original_buffer, toc_namespace, (prefix:start()), column, {}) + vim.api.nvim_buf_set_extmark(norg_buffer, toc_namespace, (prefix:start()), column, {}) ) table.insert(start_lines, (prefix:start())) local prefix_text = - module.required["core.integrations.treesitter"].get_node_text(prefix, original_buffer) + module.required["core.integrations.treesitter"].get_node_text(prefix, norg_buffer) local title_text = - vim.trim(module.required["core.integrations.treesitter"].get_node_text(title, original_buffer)) + vim.trim(module.required["core.integrations.treesitter"].get_node_text(title, norg_buffer)) if prefix_text:sub(1, 1) ~= "*" and prefix_text:match("^%W%W") then prefix_text = table.concat({ prefix_text:sub(1, 1), " " }) @@ -287,7 +290,7 @@ module.public = { prefix, title = nil, nil end end, - original_buffer + norg_buffer ) vim.bo[ui_buffer].modifiable = false @@ -298,20 +301,25 @@ module.public = { vim.api.nvim_buf_set_keymap(ui_buffer, "n", "", "", { callback = function() - local location = get_target_location_under_cursor(ui_buffer, original_buffer) + local location = get_target_location_under_cursor(ui_data) if not location then return end - vim.api.nvim_set_current_win(original_window) - vim.api.nvim_set_current_buf(original_buffer) - vim.api.nvim_win_set_cursor(original_window, { location[1] + 1, location[2] }) + local norg_window = vim.fn.bufwinid(norg_buffer) + vim.api.nvim_set_current_win(norg_window) + vim.api.nvim_set_current_buf(norg_buffer) + vim.api.nvim_win_set_cursor(norg_window, { location[1] + 1, location[2] }) if module.config.public.close_after_use then vim.api.nvim_buf_delete(ui_buffer, { force = true }) end end, }) + + if module.config.public.sync_cursorline then + module.public.update_cursor(ui_data) + end end, } @@ -325,14 +333,51 @@ local function get_max_virtcol() return result end -local function unlisten_if_closed(ui_buffer, listener) +local function get_norg_ui(norg_buffer) + local tabpage = vim.api.nvim_win_get_tabpage(vim.fn.bufwinid(norg_buffer)) + return ui_data_of_tabpage[tabpage] +end + +local function unlisten_if_closed(listener) return function(ev) - if not vim.api.nvim_buf_is_valid(ui_buffer) or not vim.api.nvim_buf_is_loaded(ui_buffer) then + if vim.tbl_isempty(ui_data_of_tabpage) then return true end - return listener(ev) + local norg_buffer = ev.buf + local ui_data = get_norg_ui(norg_buffer) + if not ui_data or vim.fn.bufwinid(ui_data.buffer) == -1 then + return + end + + return listener(norg_buffer, ui_data) + end +end + +local function create_ui(tabpage, mode) + assert(tabpage == vim.api.nvim_get_current_tabpage()) + + toc_namespace = toc_namespace or vim.api.nvim_create_namespace("neorg/toc") + local ui_buffer, ui_window = + module.required["core.ui"].create_vsplit(("toc-%d"):format(tabpage), { ft = "norg" }, mode) + + local ui_wo = vim.wo[ui_window] + ui_wo.scrolloff = 999 + ui_wo.conceallevel = 0 + ui_wo.foldmethod = "expr" + ui_wo.foldexpr = "v:lua.vim.treesitter.foldexpr()" + ui_wo.foldlevel = 99 + + if module.config.public.sync_cursorline then + ui_wo.cursorline = true end + + ui_data = { + buffer = ui_buffer, + } + + ui_data_of_tabpage[tabpage] = ui_data + return ui_data end module.on_event = function(event) @@ -341,6 +386,7 @@ module.on_event = function(event) end local toc_title = vim.split(module.public.parse_toc_macro(event.buffer) or "Table of Contents", "\n") + local norg_buffer = event.buffer if event.content and event.content[1] == "qflist" then local qflist = module.public.generate_qflist(event.buffer) @@ -357,157 +403,107 @@ module.on_event = function(event) return end - -- FIXME(vhyrro): When the buffer already exists then simply refresh the buffer - -- instead of erroring out. - toc_namespace = toc_namespace or vim.api.nvim_create_namespace("neorg/toc") - local ui_buffer, ui_window = - module.required["core.ui"].create_vsplit("toc", { ft = "norg" }, (event.content[1] or "left") == "left") - - vim.wo[ui_window].scrolloff = 999 - vim.wo[ui_window].conceallevel = 0 - vim.wo[ui_window].foldmethod = "expr" - vim.wo[ui_window].foldexpr = "v:lua.vim.treesitter.foldexpr()" - vim.wo[ui_window].foldlevel = 99 - - if module.config.public.sync_cursorline then - vim.api.nvim_win_set_option(ui_window, "cursorline", true) + local tabpage = vim.api.nvim_win_get_tabpage(vim.fn.bufwinid(norg_buffer)) + if ui_data_of_tabpage[tabpage] then + module.public.update_toc(toc_title, ui_data_of_tabpage[tabpage], norg_buffer) + return end - module.public.update_toc(toc_title, event.buffer, event.window, ui_buffer, ui_window) - if module.config.public.sync_cursorline then - module.public.update_cursor(event.buffer, ui_buffer) - end + local ui_data = ui_data_of_tabpage[tabpage] or create_ui(tabpage, (event.content[1] or "left") == "left") + + module.public.update_toc(toc_title, ui_data_of_tabpage[tabpage], norg_buffer) if module.config.public.fit_width then local max_virtcol_1bex = get_max_virtcol() - local current_winwidth = vim.fn.winwidth(ui_window) + local current_winwidth = vim.fn.winwidth(vim.fn.bufwinid(ui_data.buffer)) local new_winwidth = math.min(current_winwidth, math.max(30, max_virtcol_1bex - 1)) vim.cmd(("vertical resize %d"):format(new_winwidth + 1)) -- +1 for margin end local close_buffer_callback = function() -- Check if ui_buffer exists before deleting it - if vim.api.nvim_buf_is_loaded(ui_buffer) then - vim.api.nvim_buf_delete(ui_buffer, { force = true }) + if vim.api.nvim_buf_is_loaded(ui_data.buffer) then + vim.api.nvim_buf_delete(ui_data.buffer, { force = true }) end end - vim.api.nvim_buf_set_keymap(ui_buffer, "n", "q", "", { + vim.api.nvim_buf_set_keymap(ui_data.buffer, "n", "q", "", { callback = close_buffer_callback, }) vim.api.nvim_create_autocmd("WinClosed", { - buffer = ui_buffer, + buffer = ui_data.buffer, once = true, callback = close_buffer_callback, }) do - local norg_buffer, norg_window = event.buffer, event.window - local ui_cursor_start_moving = false - vim.api.nvim_create_autocmd("BufWritePost", { pattern = "*.norg", - callback = unlisten_if_closed(ui_buffer, function(_ev) + callback = unlisten_if_closed(function(norg_buffer, ui_data) toc_title = vim.split(module.public.parse_toc_macro(norg_buffer) or "Table of Contents", "\n") - module.public.update_toc(toc_title, norg_buffer, norg_window, ui_buffer, ui_window) - if module.config.public.sync_cursorline then - data_of_norg_win[norg_window].last_row = nil -- invalidate cursor cache - module.public.update_cursor(norg_buffer, ui_buffer) - end + data_of_norg_buf[norg_buffer].last_row = nil -- invalidate cursor cache + module.public.update_toc(toc_title, ui_data, norg_buffer) end), }) vim.api.nvim_create_autocmd("BufEnter", { pattern = "*.norg", - callback = unlisten_if_closed(ui_buffer, function(_ev) - local buf = vim.api.nvim_get_current_buf() - - if buf == ui_buffer or norg_buffer == buf then + callback = unlisten_if_closed(function(norg_buffer, ui_data) + if norg_buffer == ui_data.buffer or norg_buffer == ui_data.norg_buffer then return end - norg_buffer = buf - local norg_window = vim.fn.bufwinid(norg_buffer) - toc_title = vim.split(module.public.parse_toc_macro(buf) or "Table of Contents", "\n") - module.public.update_toc(toc_title, norg_buffer, norg_window, ui_buffer, ui_window) - if module.config.public.sync_cursorline then - module.public.update_cursor(norg_buffer, ui_buffer) - end + module.public.update_toc(toc_title, ui_data, norg_buffer) end), }) -- Sync cursor: ToC -> content if module.config.public.sync_cursorline then vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { - buffer = ui_buffer, + buffer = ui_data.buffer, callback = function(ev) - assert(ev.buf == ui_buffer) - - if not norg_buffer then - return - end + assert(ev.buf == ui_data.buffer) -- Ignore the first (fake) CursorMoved coming together with BufEnter of the ToC buffer - if ui_cursor_start_moving then - local location = get_target_location_under_cursor(ui_buffer, norg_buffer) + if ui_data.cursor_start_moving then + local location = get_target_location_under_cursor(ui_data) if location then - local ui_window = vim.fn.bufwinid(ui_buffer) - local norg_window = vim.fn.bufwinid(norg_buffer) + local ui_window = vim.fn.bufwinid(ui_data.buffer) + local norg_window = vim.fn.bufwinid(ui_data.norg_buffer) vim.api.nvim_win_set_cursor(norg_window, { location[1] + 1, location[2] }) - vim.api.nvim_set_current_win(norg_window) - vim.cmd("normal! zz") - vim.api.nvim_set_current_win(ui_window) + vim.api.nvim_buf_call(ui_data.norg_buffer, function() vim.cmd("normal! zz") end) end end - ui_cursor_start_moving = true + ui_data.cursor_start_moving = true end, }) -- Sync cursor: content -> ToC vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { pattern = "*.norg", - callback = unlisten_if_closed(ui_buffer, function(ev) - if ev.buf ~= norg_buffer then + callback = unlisten_if_closed(function(norg_buffer, ui_data) + if norg_buffer ~= ui_data.norg_buffer then return end - if not data_of_norg_win[vim.fn.bufwinid(norg_buffer)] then + if not data_of_norg_buf[norg_buffer] then -- toc not yet created because BufEnter is not yet triggered return end - module.public.update_cursor(norg_buffer, ui_buffer) + module.public.update_cursor(ui_data) end), }) - -- Ignore the first (fake) CursorMoved coming together with BufEnter of the ToC buffer - vim.api.nvim_create_autocmd("BufEnter", { - buffer = ui_buffer, - callback = function(_ev) - ui_cursor_start_moving = false - end, - }) - -- When leaving the content buffer, add its last cursor position to jump list vim.api.nvim_create_autocmd("BufLeave", { pattern = "*.norg", - callback = unlisten_if_closed(ui_buffer, function(_ev) + callback = unlisten_if_closed(function(_norg_buffer, _ui_data) vim.cmd("normal! m'") end), }) - - vim.api.nvim_create_autocmd("BufHidden", { - pattern = "*.norg", - callback = unlisten_if_closed(ui_buffer, function(ev) - if ev.buf == norg_buffer then - norg_buffer = nil - norg_window = nil - return - end - end), - }) end end end