notex.nvim/lua/notex/ui/buffer.lua

561 lines
15 KiB
Lua
Raw Permalink Normal View History

2025-10-05 20:16:33 -04:00
-- 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, " <Enter> - 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 {
["<CR>"] = {
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"
},
["<Esc>"] = {
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 <Esc> 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:
<Enter> - Open document under cursor
o - Open document in new tab
q - Close this view
<Esc> - 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