561 lines
15 KiB
Lua
561 lines
15 KiB
Lua
|
-- 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
|