-- Virtual buffer management module local M = {} local utils = require('notex.utils') -- Active buffer management local active_buffers = {} local buffer_configs = {} -- Create virtual buffer for query results function M.create_query_buffer(query_results, options) options = options or {} local buffer_id = vim.api.nvim_create_buf(false, true) -- unlisted, scratch buffer if not buffer_id then return nil, "Failed to create virtual buffer" end -- Set buffer options M.setup_buffer_options(buffer_id, options) -- Generate buffer content local lines, syntax = M.generate_buffer_content(query_results, options) -- Set buffer content vim.api.nvim_buf_set_lines(buffer_id, 0, -1, false, lines) -- Set syntax highlighting if syntax then vim.api.nvim_buf_set_option(buffer_id, "filetype", syntax) end -- Create window if requested local window_id if options.create_window ~= false then window_id = M.create_query_window(buffer_id, options) end -- Store buffer configuration local config = { buffer_id = buffer_id, window_id = window_id, query_results = query_results, options = options, created_at = os.time(), mappings = M.setup_buffer_mappings(buffer_id, options) } active_buffers[buffer_id] = config buffer_configs[buffer_id] = config return config end -- Setup buffer options function M.setup_buffer_options(buffer_id, options) local buf_opts = { buftype = "nofile", swapfile = false, bufhidden = "wipe", modifiable = options.modifiable or false, readonly = not (options.modifiable or false), textwidth = 0, wrapmargin = 0, wrap = options.wrap or false } for opt, value in pairs(buf_opts) do vim.api.nvim_buf_set_option(buffer_id, opt, value) end -- Set buffer name local buf_name = options.name or "notex://query-results" vim.api.nvim_buf_set_name(buffer_id, buf_name) end -- Generate buffer content from query results function M.generate_buffer_content(query_results, options) local lines = {} local syntax = "notex" -- Add header table.insert(lines, "Query Results") table.insert(lines, string.rep("=", 50)) table.insert(lines, "") -- Add query metadata if query_results.query_string then table.insert(lines, "Query: " .. query_results.query_string) table.insert(lines, "") end -- Add execution statistics table.insert(lines, string.format("Found %d documents (%.2fms)", query_results.total_count or 0, query_results.execution_time_ms or 0)) table.insert(lines, "") -- Add document results if query_results.documents and #query_results.documents > 0 then lines = M.add_document_table(lines, query_results.documents, options) else table.insert(lines, "No documents found matching the query criteria.") table.insert(lines, "") end -- Add help section lines = M.add_help_section(lines, options) return lines, syntax end -- Add document table to buffer function M.add_document_table(lines, documents, options) local max_width = options.max_width or 120 local include_properties = options.include_properties or {"title", "status", "priority", "created_at"} -- Calculate column widths local column_widths = M.calculate_column_widths(documents, include_properties, max_width) -- Add table header local header_parts = {"#", "File"} for _, prop in ipairs(include_properties) do table.insert(header_parts, M.format_column_header(prop, column_widths[prop])) end table.insert(lines, table.concat(header_parts, " | ")) -- Add separator local separator_parts = {"-", string.rep("-", 20)} for _, prop in ipairs(include_properties) do table.insert(separator_parts, string.rep("-", column_widths[prop])) end table.insert(lines, table.concat(separator_parts, " | ")) table.insert(lines, "") -- Add document rows for i, doc in ipairs(documents) do local row_parts = {tostring(i), M.truncate_path(doc.file_path, 20)} for _, prop in ipairs(include_properties) do local value = doc.properties and doc.properties[prop] or "" local formatted_value = M.format_property_value(value, column_widths[prop]) table.insert(row_parts, formatted_value) end table.insert(lines, table.concat(row_parts, " | ")) end table.insert(lines, "") return lines end -- Calculate column widths for table function M.calculate_column_widths(documents, properties, max_width) local widths = {} -- Set minimum widths based on property names for _, prop in ipairs(properties) do widths[prop] = math.max(#prop, 10) end -- Adjust based on content for _, doc in ipairs(documents) do for _, prop in ipairs(properties) do local value = doc.properties and doc.properties[prop] or "" local formatted = tostring(value) widths[prop] = math.max(widths[prop], #formatted) end end -- Limit maximum width local total_min_width = 30 -- # + File columns local available_width = max_width - total_min_width if #properties > 0 then local per_column = math.floor(available_width / #properties) for _, prop in ipairs(properties) do widths[prop] = math.min(widths[prop], per_column) end end return widths end -- Format column header function M.format_column_header(property, width) local formatted = property:gsub("_", " "):gsub("(%a)([%w_]*)", function(first, rest) return first:upper() .. rest:lower() end) return M.pad_right(formatted, width) end -- Format property value for table function M.format_property_value(value, width) if not value then return M.pad_right("", width) end local formatted = tostring(value) -- Truncate if too long if #formatted > width then formatted = formatted:sub(1, width - 3) .. "..." end return M.pad_right(formatted, width) end -- Truncate file path function M.truncate_path(path, max_length) if #path <= max_length then return M.pad_right(path, max_length) end local filename = vim.fn.fnamemodify(path, ":t") local dirname = vim.fn.fnamemodify(path, ":h") if #filename + 3 <= max_length then local dir_length = max_length - #filename - 3 local truncated_dir = dirname:sub(-dir_length) return M.pad_right("..." .. truncated_dir .. "/" .. filename, max_length) else return M.pad_right("..." .. filename:sub(-(max_length - 3)), max_length) end end -- Pad string to specified width function M.pad_right(str, width) return str .. string.rep(" ", width - #str) end -- Add help section function M.add_help_section(lines, options) if options.show_help == false then return lines end table.insert(lines, "Help:") table.insert(lines, " - Open document under cursor") table.insert(lines, " o - Open document in new tab") table.insert(lines, " e - Edit document properties") table.insert(lines, " s - Save query") table.insert(lines, " r - Refresh results") table.insert(lines, " q - Close this view") table.insert(lines, "") table.insert(lines, "Press ? for more help") return lines end -- Create window for buffer function M.create_query_window(buffer_id, options) local window_config = options.window or {} -- Default window configuration local default_config = { relative = "editor", width = math.min(120, vim.api.nvim_get_option_value("columns", {})), height = math.min(30, vim.api.nvim_get_option_value("lines", {}) - 5), row = 1, col = 1, border = "rounded", style = "minimal", title = " Query Results ", title_pos = "center" } -- Merge with user config local final_config = vim.tbl_deep_extend("force", default_config, window_config) local window_id = vim.api.nvim_open_win(buffer_id, true, final_config) if not window_id then return nil, "Failed to create window" end -- Set window options vim.api.nvim_win_set_option(window_id, "wrap", false) vim.api.nvim_win_set_option(window_id, "cursorline", true) vim.api.nvim_win_set_option(window_id, "number", false) vim.api.nvim_win_set_option(window_id, "relativenumber", false) vim.api.nvim_win_set_option(window_id, "signcolumn", "no") return window_id end -- Setup buffer mappings function M.setup_buffer_mappings(buffer_id, options) local mappings = options.mappings or M.get_default_mappings() for key, action in pairs(mappings) do local mode = action.mode or "n" local opts = { buffer = buffer_id, noremap = true, silent = true, nowait = true } if type(action.callback) == "string" then vim.keymap.set(mode, key, action.callback, opts) elseif type(action.callback) == "function" then vim.keymap.set(mode, key, action.callback, opts) end end return mappings end -- Get default mappings function M.get_default_mappings() return { [""] = { callback = function() require('notex.ui.buffer').handle_enter_key() end, description = "Open document" }, ["o"] = { callback = function() require('notex.ui.buffer').handle_open_tab() end, description = "Open in new tab" }, ["e"] = { callback = function() require('notex.ui.buffer').handle_edit_mode() end, description = "Edit properties" }, ["s"] = { callback = function() require('notex.ui.buffer').handle_save_query() end, description = "Save query" }, ["r"] = { callback = function() require('notex.ui.buffer').handle_refresh() end, description = "Refresh results" }, ["q"] = { callback = function() require('notex.ui.buffer').handle_close_buffer() end, description = "Close buffer" }, ["?"] = { callback = function() require('notex.ui.buffer').show_help() end, description = "Show help" }, [""] = { callback = function() require('notex.ui.buffer').handle_close_buffer() end, description = "Close buffer" } } end -- Handle Enter key - open document function M.handle_enter_key() local line = vim.api.nvim_get_current_line() local cursor = vim.api.nvim_win_get_cursor(0) local line_num = cursor[1] -- Skip header lines if line_num < 6 then return end -- Extract file path from line local file_path = M.extract_file_path_from_line(line) if file_path and utils.file_exists(file_path) then vim.cmd('edit ' .. file_path) else vim.notify("Cannot open file: " .. (file_path or "unknown"), vim.log.levels.WARN) end end -- Handle open in new tab function M.handle_open_tab() local line = vim.api.nvim_get_current_line() local file_path = M.extract_file_path_from_line(line) if file_path and utils.file_exists(file_path) then vim.cmd('tabedit ' .. file_path) else vim.notify("Cannot open file: " .. (file_path or "unknown"), vim.log.levels.WARN) end end -- Handle edit mode function M.handle_edit_mode() local buffer = vim.api.nvim_get_current_buf() vim.api.nvim_buf_set_option(buffer, "modifiable", true) vim.notify("Edit mode enabled - press to save and exit", vim.log.levels.INFO) end -- Handle save query function M.handle_save_query() local config = buffer_configs[vim.api.nvim_get_current_buf()] if not config or not config.query_results then return end vim.ui.input({ prompt = "Query name: " }, function(query_name) if query_name and query_name ~= "" then local query_engine = require('notex.query') local result = query_engine.save_query(query_name, config.query_results.query_string) if result.success then vim.notify("Query saved: " .. query_name, vim.log.levels.INFO) else vim.notify("Failed to save query: " .. result.error, vim.log.levels.ERROR) end end end) end -- Handle refresh function M.handle_refresh() local config = buffer_configs[vim.api.nvim_get_current_buf()] if not config or not config.query_results or not config.query_results.query_string then return end -- Re-execute query local query_engine = require('notex.query') local result = query_engine.execute_query(config.query_results.query_string) if result.success then -- Update buffer content local lines, _ = M.generate_buffer_content(result, config.options) vim.api.nvim_buf_set_lines(vim.api.nvim_get_current_buf(), 0, -1, false, lines) -- Update config config.query_results = result vim.notify("Query refreshed", vim.log.levels.INFO) else vim.notify("Failed to refresh query: " .. table.concat(result.errors, ", "), vim.log.levels.ERROR) end end -- Handle close buffer function M.handle_close_buffer() local buffer = vim.api.nvim_get_current_buf() local config = active_buffers[buffer] if config and config.window_id then vim.api.nvim_win_close(config.window_id, true) else vim.cmd('bdelete!') end end -- Show help function M.show_help() local help_content = [[ Notex Query Results Help: Navigation: - Open document under cursor o - Open document in new tab q - Close this view - Close this view Actions: e - Enable edit mode for modifying results s - Save current query for reuse r - Refresh query results Other: ? - Show this help j/k - Move up/down gg/G - Go to top/bottom /pattern - Search in results Press any key to close this help ]] local buf = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(help_content, "\n")) vim.api.nvim_buf_set_option(buf, "filetype", "help") local win_id = vim.api.nvim_open_win(buf, true, { relative = "editor", width = 60, height = 20, row = math.floor((vim.api.nvim_get_option_value("lines", {}) - 20) / 2), col = math.floor((vim.api.nvim_get_option_value("columns", {}) - 60) / 2), border = "rounded", style = "minimal", title = " Help " }) vim.api.nvim_win_set_option(win_id, "wrap", true) -- Close help on any key vim.api.nvim_create_autocmd("CursorMoved,WinLeave", { buffer = buf, once = true, callback = function() vim.api.nvim_win_close(win_id, true) end }) end -- Extract file path from line function M.extract_file_path_from_line(line) -- Match file path in table row local match = line:match("^%s*%d+%s+|?%s*([^|]+)") if match then -- Clean up the path local path = match:trim() path = path:gsub("%s+$", "") -- Remove trailing spaces return path end return nil end -- Get active buffer configurations function M.get_active_buffers() local configs = {} for buffer_id, config in pairs(active_buffers) do if vim.api.nvim_buf_is_valid(buffer_id) then configs[buffer_id] = config else -- Clean up invalid buffers active_buffers[buffer_id] = nil buffer_configs[buffer_id] = nil end end return configs end -- Clean up inactive buffers function M.cleanup_buffers() local to_remove = {} for buffer_id, config in pairs(active_buffers) do if not vim.api.nvim_buf_is_valid(buffer_id) then table.insert(to_remove, buffer_id) end end for _, buffer_id in ipairs(to_remove) do active_buffers[buffer_id] = nil buffer_configs[buffer_id] = nil end end return M