diff --git a/README.md b/README.md index e85f13f..8b7653b 100644 --- a/README.md +++ b/README.md @@ -314,8 +314,15 @@ enable_icons = true, -- What keys to search for matches. search_keys = { "author", "editor", "year", "title", "tags" }, - -- The format for the previewer. Each line in the config represents a line in - -- the preview. For each line, we define: + -- Papis.nvim uses a common configuration format for defining the formatting + -- of strings. Sometimes -- as for instance in the below `preview_format` option -- + -- we define a set of lines. At other times -- as for instance in the `results_format` + -- option -- we define a single line. Sets of lines are composed of single lines. + -- A line can be composed of either a single element or multiple elements. The below + -- `preview_format` shows an example where each line is defined by a table with just + -- one element. The `results_format` and `popup_format` are examples where (some) of + -- the lines contain multiple elements (and are represented by a table of tables). + -- Each element contains: -- 1. The key whose value is shown -- 2. How it is formatted (here, each is just given as is) -- 3. The highlight group @@ -324,10 +331,10 @@ enable_icons = true, -- formatting of the key and its highlight group. The key is shown *before* -- the value in the preview (even though it is defined after it in this -- configuration (e.g. `title = Critique of Pure Reason`)). - -- `empty_line` is used to insert an empty line + -- An element may also just contain `empty_line`. This is used to insert an empty line -- Strings that define the formatting (such as in 2. and 4. above) can optionally - -- be a table, defining, first, an icon, and second, a non-icon version. What is - -- used is defined by the `enable_icons` option. + -- be a table, defining, first, an icon, and second, a non-icon version. The + -- `enable_icons` option determines what is used. preview_format = { { "author", "%s", "PapisPreviewAuthor" }, { "year", "%s", "PapisPreviewYear" }, @@ -357,10 +364,18 @@ enable_icons = true, ["at-cursor"] = { -- The format of the popup shown on `:Papis at-cursor show-popup` (equivalent to points 1-3 - -- of `preview_format`) + -- of `preview_format`). Note that one of the lines is composed of multiple elements. Note + -- also the `{ "vspace", "vspace" },` line which is exclusive to `popup_format` and which tells + -- papis.nvim to fill the space between the previous and next element with whitespace (and + -- in effect make whatever comes after right-aligned). It can only occur once in a line. popup_format = { - { "author", "%s", "PapisPopupAuthor" }, - { "year", "%s", "PapisPopupYear" }, + { + { "author", "%s", "PapisPopupAuthor" }, + { "vspace", "vspace" }, + { "files", { " ", "F " }, "PapisResultsFiles" }, + { "notes", { "󰆈 ", "N " }, "PapisResultsNotes" }, + }, + { "year", "%s", "PapisPopupYear" }, { "title", "%s", "PapisPopupTitle" }, }, }, diff --git a/lua/papis/at-cursor/init.lua b/lua/papis/at-cursor/init.lua index 76663b1..0e8c3f5 100644 --- a/lua/papis/at-cursor/init.lua +++ b/lua/papis/at-cursor/init.lua @@ -21,7 +21,6 @@ local db = require("papis.sqlite-wrapper") if not db then return nil end -local hover_required_db_keys = utils:get_required_db_keys({ popup_format }) ---Tries to identify the ref under cursor ---@return string|nil #Nil if nothing is found, otherwise is the identified ref @@ -72,9 +71,8 @@ end ---Creates a popup with information regarding the entry specified by `ref` ---@param papis_id string #The `papis_id` of the entry local function create_hover_popup(papis_id) - local entry = db.data:get({ papis_id = papis_id }, hover_required_db_keys)[1] - local clean_popup_format = utils.do_clean_format_tbl(popup_format, entry) - local popup_lines, width = utils.make_nui_lines(clean_popup_format, entry) + local entry = db.data:get({ papis_id = papis_id })[1] + local popup_lines, width = utils:make_nui_lines(popup_format, entry) local popup = NuiPopup({ position = 1, diff --git a/lua/papis/config.lua b/lua/papis/config.lua index e953d38..b4b514c 100644 --- a/lua/papis/config.lua +++ b/lua/papis/config.lua @@ -37,6 +37,7 @@ local default_config = { editor = "text", year = "text", title = "text", + shorttitle = "text", type = "text", abstract = "text", time_added = "text", @@ -105,9 +106,14 @@ local default_config = { }, ["at-cursor"] = { popup_format = { - { "author", "%s", "PapisPopupAuthor" }, - { "year", "%s", "PapisPopupYear" }, - { "title", "%s", "PapisPopupTitle" }, + { + { "author", "%s", "PapisPopupAuthor" }, + { "vspace", "vspace" }, + { "files", { " ", "F " }, "PapisResultsFiles" }, + { "notes", { "󰆈 ", "N " }, "PapisResultsNotes" }, + }, + { "year", "%s", "PapisPopupYear" }, + { "title", "%s", "PapisPopupTitle" }, }, }, ["search"] = { diff --git a/lua/papis/search/data.lua b/lua/papis/search/data.lua index fcf5a9c..9e92b36 100644 --- a/lua/papis/search/data.lua +++ b/lua/papis/search/data.lua @@ -101,7 +101,7 @@ local function init_tbl() local entry = db["data"]:__get({ where = { id = id } })[1] - local display_strings = utils:format_display_strings(entry, results_format) + local display_strings = utils:format_display_strings(entry, results_format, false, true) local search_string = format_search_string(entry) local items = {} diff --git a/lua/papis/utils.lua b/lua/papis/utils.lua index 8b94351..cf794d9 100644 --- a/lua/papis/utils.lua +++ b/lua/papis/utils.lua @@ -225,107 +225,72 @@ function M:do_open_text_file(papis_id, type) vim.cmd(cmd) end ----Takes the format table and removes k = v pairs not existing in the entry + some other conditions ----@param format_table table #As defined in config.lua (e.g. "preview_format") ----@param entry table #An entry ----@param remove_editor_if_author boolean? #If true we don't add the editor if the entry has an author ----@return table #Same format as `format_table` but with k = v pairs removed -function M.do_clean_format_tbl(format_table, entry, remove_editor_if_author) - local enable_icons = require("papis.config")["enable_icons"] - local clean_format_table = {} - for _, v in ipairs(format_table) do - local f = vim.deepcopy(v) -- TODO: check if deepcopy necessary - -- add entry value if either there's an entry value corresponding to the value in the - -- format table or the value in the format table is "empty_line" - if entry[f[1]] or f[1] == "empty_line" then - clean_format_table[#clean_format_table + 1] = f - -- don't add editor if there is author and use_author_if_editor is true - elseif remove_editor_if_author and f[1] == "author" and entry["editor"] then - clean_format_table[#clean_format_table + 1] = f - -- add empty space if space is forced but the element doesn't exist for entry - elseif f[4] == "force_space" then - f[2] = " " -- TODO: this only works for icons, hardcoded because luajit doesn't support utf8.len - clean_format_table[#clean_format_table + 1] = f - end - -- use either icons or normal characters depending on settings - if type(f[2]) == "table" then - if enable_icons then - f[2] = f[2][1] - else - f[2] = f[2][2] - end - end - if type(f[5]) == "table" then - if enable_icons then - f[5] = f[5][1] - else - f[5] = f[5][2] - end - end - end - return clean_format_table -end - ---Makes nui lines ready to be displayed ----@param clean_format_tbl table #A cleaned format table as output by self.do_clean_format_tbl +---@param lines_format_tbl table #A format table defining multiple lines ---@param entry table #An entry ---@return table #A list of nui lines ---@return integer #The maximum character length of the nui lines -function M.make_nui_lines(clean_format_tbl, entry) - local nui_lines = {} +function M:make_nui_lines(lines_format_tbl, entry) + local lines = {} + local line_widths = {} local max_width = 0 - for _, v in ipairs(clean_format_tbl) do - local line = NuiLine() - local width1 = 0 - local width2 = 0 - if v[1] == "empty_line" then - line:append(" ") + -- local max_width_line_nr + local vspace = {} + for _, line_format_tbl in ipairs(lines_format_tbl) do + local line = {} + local width = 0 + if line_format_tbl[1] == "empty_line" then + -- here we add a line without hl group + line[#line + 1] = { " " } else - if v[4] == "show_key" then - local str = v[1] - str = string.format(v[5], str) - str = string.gsub(str, "\n", "") - width1 = vim.fn.strdisplaywidth(str, 1) - line:append(str, v[6]) - end - if type(entry[v[1]]) ~= "table" then - local str = tostring(entry[v[1]]) - str = string.format(v[2], str) - str = string.gsub(str, "\n", "") - width2 = vim.fn.strdisplaywidth(str, 1) - line:append(str, v[3]) - else - local str = table.concat(entry[v[1]], ", ") - str = string.format(v[2], str) - str = string.gsub(str, "\n", "") - width2 = vim.fn.strdisplaywidth(str, 1) - line:append(str, v[3]) + -- we format the strings for the line and add them to the line + local formatted_strings = self:format_display_strings(entry, line_format_tbl) + for k, v in ipairs(formatted_strings) do + line[#line + 1] = { v[1], v[2] } + if v[1] == "vspace" then + -- in case of vspace elements, we gotta keep track where they occur + vspace[#vspace + 1] = { linenr = #lines + 1, elem = k } + else + -- we get the width of the line by adding all elements' width + width = width + vim.fn.strdisplaywidth(v[1], 1) + end end end - max_width = math.max(max_width, (width1 + width2)) - nui_lines[#nui_lines + 1] = line + -- add the width of the line just processed to the table of line_widths + line_widths[#lines + 1] = width + + -- add the line just processed to the table of lines + lines[#lines + 1] = line end - return nui_lines, max_width -end + max_width = math.max(unpack(line_widths)) ----Get the list of keys required by format table ----@param tbls table #A format table(e.g. "preview_format" in config.lua) ----@return table #A list of keys -function M:get_required_db_keys(tbls) - local required_db_keys = { id = true } - for _, tbl in ipairs(tbls) do - for _, v in ipairs(tbl) do - if v[1] == nil then - required_db_keys[v] = true - else - required_db_keys[v[1]] = true - end + local vspace_len = 0 + -- sort out vertical space padding for each line that has `vspace` + for _, v in pairs(vspace) do + if line_widths[v.linenr] >= max_width then + -- if the line with the vspace is the longest line, only add 1 space + vspace_len = 1 + else + -- if it isn't the longest line, calculate required vspace + vspace_len = max_width - (line_widths[v.linenr]) end + -- replace "vspace" by the required number of " " + lines[v.linenr][v.elem] = { string.rep(" ", vspace_len) } + -- and recalculate max_width + max_width = math.max(max_width, (line_widths[v.linenr] + vspace_len)) end - required_db_keys["empty_line"] = nil - required_db_keys = vim.tbl_keys(required_db_keys) - return required_db_keys + + -- turn our lines into NuiLines + local nui_lines = {} + for _, line in ipairs(lines) do + local nui_line = NuiLine() + for _, v in ipairs(line) do + nui_line:append(v[1], v[2]) + end + nui_lines[#nui_lines + 1] = nui_line + end + return nui_lines, max_width end ---Determine whether there's a process with a given pid @@ -356,21 +321,48 @@ end ---Creates a table of formatted strings to be displayed in a line (e.g. Telescope results pane) ---@param entry table #A papis entry +---@param line_format_tbl table #A table containing format strings defining the line ---@param use_shortitle? boolean #If true, use short titles ----@return table #A list of strings -function M:format_display_strings(entry, format_table, use_shortitle) - local clean_results_format = self.do_clean_format_tbl(format_table, entry, true) - - local str_elements = {} - for _, v in ipairs(clean_results_format) do - assert(v ~= "empty_line", "Empty lines aren't allowed for the results_format") - if v[1] == "author" then +---@param remove_editor_if_author? boolean #If true, remove editor if author exists +---@return table #A list of lists like { { "formatted string", "HighlightGroup", {opts} }, ... } +function M:format_display_strings(entry, line_format_tbl, use_shortitle, remove_editor_if_author) + local enable_icons = require("papis.config")["enable_icons"] + + -- if the line has just one item, embed within a tbl so we can process like the others + if type(line_format_tbl[1]) == "string" then + log.debug("line has just one item, embed within table") + line_format_tbl = { line_format_tbl } + end + + ---Table containing tables {format string, string, highlight group} + ---@type table> + local formatting_items = {} + + -- iterate over each string element in the line_format_tbl + for _, line_item in ipairs(line_format_tbl) do + local line_item_copy = vim.deepcopy(line_item) + + -- set icons or normal chars as desired + local icon_keys = { 2, 5 } + for _, icon_key in ipairs(icon_keys) do + if type(line_item_copy[icon_key]) == "table" then + if enable_icons then + line_item_copy[icon_key] = line_item_copy[icon_key][1] + else + line_item_copy[icon_key] = line_item_copy[icon_key][2] + end + end + end + + -- format values + local processed_string = nil + if line_item_copy[1] == "author" and (entry["author"] or entry["author_list"] or entry["editor"]) then -- add author local authors = {} if entry["author_list"] then for _, vv in ipairs(entry["author_list"]) do authors[#authors + 1] = vv["family"] end - str_elements[#str_elements + 1] = table.concat(authors, ", ") + processed_string = table.concat(authors, ", ") elseif entry["author"] then if string.find(entry["author"], " and ") then local str = string.gsub(entry["author"], " and ", "|") @@ -381,7 +373,7 @@ function M:format_display_strings(entry, format_table, use_shortitle) else authors[#authors + 1] = self.do_split_str(entry["author"], ",")[1] end - str_elements[#str_elements + 1] = table.concat(authors, ", ") + processed_string = table.concat(authors, ", ") elseif entry["editor"] then if string.find(entry["editor"], " and ") then local str = string.gsub(entry["editor"], " and ", "|") @@ -392,27 +384,68 @@ function M:format_display_strings(entry, format_table, use_shortitle) else authors[#authors + 1] = self.do_split_str(entry["editor"], ",")[1] end - str_elements[#str_elements + 1] = table.concat(authors, ", ") .. " (eds.)" + processed_string = table.concat(authors, ", ") .. " (eds.)" end - elseif v[1] == "title" and use_shortitle then - local shortitle = entry["title"]:match("([^:]+)") - str_elements[#str_elements + 1] = shortitle - else - if entry[v[1]] then - str_elements[#str_elements + 1] = entry[v[1]] - elseif v[4] == "force_space" then - str_elements[#str_elements + 1] = "dummy" + elseif line_item_copy[1] == "editor" and entry["editor"] then + if not remove_editor_if_author then + local editors = {} + if string.find(entry["editor"], " and ") then + local str = string.gsub(entry["editor"], " and ", "|") + local str_split = self.do_split_str(str, "|") + for _, s in ipairs(str_split) do + editors[#editors + 1] = self.do_split_str(s, ",")[1] + end + else + editors[#editors + 1] = self.do_split_str(entry["editor"], ",")[1] + end + processed_string = table.concat(editors, ", ") + end + elseif line_item_copy[1] == "title" and (entry["title"] or entry["shortitle"]) then + if use_shortitle then + local shortitle = entry["shortitle"] or entry["title"]:match("([^:]+)") + processed_string = shortitle + else + processed_string = entry["title"] + end + elseif entry[line_item_copy[1]] then -- add other elements if they exist in the entry + local input = entry[line_item_copy[1]] + if line_item_copy[1] == ("notes" or "files") then + -- get only file names (not full path) + input = self.get_filenames(entry[line_item_copy[1]]) + end + if type(input) == "table" then + -- if it's a table, convert to string + processed_string = table.concat(entry[line_item_copy[1]], ", ") + else + processed_string = input + end + elseif line_item_copy[4] == "force_space" then + -- set icon to empty space + line_item_copy[2] = " " -- TODO: this only works for icons, hardcoded because luajit doesn't support utf8.len + -- add dummy element + processed_string = "dummy" + elseif line_item_copy[1] == "vspace" then + processed_string = "vspace" + end + + -- if a string exists, add keys if required and add hl group etc + if processed_string then + if line_item_copy[4] == "show_key" then + formatting_items[#formatting_items + 1] = { line_item_copy[5], line_item_copy[1], line_item_copy[6] } end + formatting_items[#formatting_items + 1] = { line_item_copy[2], processed_string, line_item_copy[3] } end end - local display_strings = {} - for k, str_element in ipairs(str_elements) do - local formatted_str = string.format(clean_results_format[k][2], str_element) - display_strings[#display_strings + 1] = { formatted_str, clean_results_format[k][3] } + ---Table containing tables {formatted string, highlight group} + ---@type table> + local formatted_str_and_hl = {} + for _, formatting_item in ipairs(formatting_items) do + local formatted_str = string.format(formatting_item[1], formatting_item[2]) + formatted_str_and_hl[#formatted_str_and_hl + 1] = { formatted_str, formatting_item[3] } end - return display_strings + return formatted_str_and_hl end return M diff --git a/lua/telescope/_extensions/papis.lua b/lua/telescope/_extensions/papis.lua index c6f65ff..903fb73 100644 --- a/lua/telescope/_extensions/papis.lua +++ b/lua/telescope/_extensions/papis.lua @@ -76,18 +76,7 @@ local function papis_picker(opts) }), previewer = previewers.new_buffer_previewer({ define_preview = function(self, entry, status) - local previewer_entry = vim.deepcopy(entry) - local clean_preview_format = utils.do_clean_format_tbl(preview_format, previewer_entry.id) - - -- get only file names (not full path) - if previewer_entry.id.notes then - previewer_entry.id.notes = utils.get_filenames(previewer_entry.id.notes) - end - if previewer_entry.id.files then - previewer_entry.id.files = utils.get_filenames(previewer_entry.id.files) - end - - local preview_lines = utils.make_nui_lines(clean_preview_format, previewer_entry.id) + local preview_lines = utils:make_nui_lines(preview_format, entry.id) for line_nr, line in ipairs(preview_lines) do line:render(self.state.bufnr, -1, line_nr)