Initial vibecoded proof of concept

This commit is contained in:
Alex Selimov 2025-10-05 20:16:33 -04:00
parent 74812459af
commit 461318a656
Signed by: aselimov
GPG key ID: 3DDB9C3E023F1F31
61 changed files with 13306 additions and 0 deletions

561
lua/notex/ui/buffer.lua Normal file
View file

@ -0,0 +1,561 @@
-- 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

572
lua/notex/ui/editor.lua Normal file
View file

@ -0,0 +1,572 @@
-- Inline editing interface module
local M = {}
local buffer_manager = require('notex.ui.buffer')
local database = require('notex.database.schema')
local parser = require('notex.parser')
local utils = require('notex.utils')
-- Editor state
local active_editors = {}
-- Start editing document properties
function M.start_edit_mode(buffer_id, line_number, column_number)
local config = buffer_manager.get_active_buffers()[buffer_id]
if not config then
return false, "Buffer not found"
end
-- Get document at cursor position
local doc_info = M.get_document_at_position(buffer_id, line_number, column_number)
if not doc_info then
return false, "No document found at cursor position"
end
-- Parse document to get current properties
local parse_result, parse_err = parser.parse_document(doc_info.file_path)
if not parse_result then
return false, "Failed to parse document: " .. parse_err
end
-- Create editor session
local editor_id = utils.generate_id()
local editor_session = {
id = editor_id,
buffer_id = buffer_id,
document_id = doc_info.document_id,
file_path = doc_info.file_path,
original_properties = vim.deepcopy(parse_result.properties),
current_properties = vim.deepcopy(parse_result.properties),
parse_result = parse_result,
created_at = os.time(),
modified = false
}
active_editors[editor_id] = editor_session
-- Switch buffer to editable mode
vim.api.nvim_buf_set_option(buffer_id, "modifiable", true)
vim.api.nvim_buf_set_option(buffer_id, "modified", false)
-- Update buffer content for editing
M.update_buffer_for_editing(buffer_id, editor_session)
-- Setup editor-specific mappings
M.setup_editor_mappings(buffer_id, editor_id)
utils.log("INFO", "Started edit mode for document", {
document_id = doc_info.document_id,
file_path = doc_info.file_path
})
return true, editor_id
end
-- Get document at position
function M.get_document_at_position(buffer_id, line_number, column_number)
local config = buffer_manager.get_active_buffers()[buffer_id]
if not config or not config.query_results.documents then
return nil
end
-- Simple mapping: line numbers correspond to document indices (accounting for headers)
local doc_index = line_number - 6 -- Account for header lines
if doc_index > 0 and doc_index <= #config.query_results.documents then
local doc = config.query_results.documents[doc_index]
return {
document_id = doc.id,
file_path = doc.file_path,
document = doc,
line_number = line_number,
column_number = column_number
}
end
return nil
end
-- Update buffer for editing
function M.update_buffer_for_editing(buffer_id, editor_session)
local lines = {}
-- Editor header
table.insert(lines, "Edit Mode - Document Properties")
table.insert(lines, string.rep("=", 50))
table.insert(lines, "")
table.insert(lines, string.format("File: %s", editor_session.file_path))
table.insert(lines, string.format("Modified: %s", editor_session.modified and "Yes" or "No"))
table.insert(lines, "")
table.insert(lines, "Properties (edit values, press <Enter> to save):")
table.insert(lines, "")
-- Property list
if editor_session.current_properties then
local sorted_props = {}
for key, value in pairs(editor_session.current_properties) do
table.insert(sorted_props, {key = key, value = value})
end
table.sort(sorted_props, function(a, b) return a.key < b.key end)
for i, prop in ipairs(sorted_props) do
local line = string.format("%-20s = %s", prop.key .. ":", tostring(prop.value))
table.insert(lines, line)
end
end
table.insert(lines, "")
-- Editor help
table.insert(lines, "Editor Commands:")
table.insert(lines, " <Enter> - Save changes")
table.insert(lines, " <Esc> - Cancel changes")
table.insert(lines, " a - Add new property")
table.insert(lines, " d - Delete property at cursor")
table.insert(lines, " u - Undo changes")
table.insert(lines, "")
-- Set buffer content
vim.api.nvim_buf_set_lines(buffer_id, 0, -1, false, lines)
-- Move cursor to first property
vim.api.nvim_win_set_cursor(0, {7, 0})
end
-- Setup editor mappings
function M.setup_editor_mappings(buffer_id, editor_id)
local opts = {
buffer = buffer_id,
noremap = true,
silent = true
}
-- Save and exit
vim.keymap.set("n", "<Enter>", function()
M.save_and_exit_edit_mode(buffer_id, editor_id)
end, opts)
vim.keymap.set("i", "<Enter>", function()
M.save_and_exit_edit_mode(buffer_id, editor_id)
end, opts)
-- Cancel editing
vim.keymap.set("n", "<Esc>", function()
M.cancel_edit_mode(buffer_id, editor_id)
end, opts)
vim.keymap.set("i", "<Esc>", function()
M.cancel_edit_mode(buffer_id, editor_id)
end, opts)
-- Add new property
vim.keymap.set("n", "a", function()
M.add_new_property(buffer_id, editor_id)
end, opts)
-- Delete property
vim.keymap.set("n", "d", function()
M.delete_property_at_cursor(buffer_id, editor_id)
end, opts)
-- Undo changes
vim.keymap.set("n", "u", function()
M.undo_changes(buffer_id, editor_id)
end, opts)
end
-- Save and exit edit mode
function M.save_and_exit_edit_mode(buffer_id, editor_id)
local editor_session = active_editors[editor_id]
if not editor_session then
return
end
-- Parse edited properties from buffer
local edited_properties = M.parse_properties_from_buffer(buffer_id)
if not edited_properties then
vim.notify("Failed to parse edited properties", vim.log.levels.ERROR)
return
end
-- Check if anything changed
local changes = M.detect_property_changes(editor_session.original_properties, edited_properties)
if #changes == 0 then
vim.notify("No changes detected", vim.log.levels.INFO)
M.exit_edit_mode(buffer_id, editor_id)
return
end
-- Update database
local ok, result = M.update_document_properties(editor_session.document_id, changes)
if not ok then
vim.notify("Failed to update properties: " .. result, vim.log.levels.ERROR)
return
end
-- Update file on disk
local file_ok, file_result = M.update_yaml_file(editor_session.file_path, edited_properties)
if not file_ok then
vim.notify("Failed to update file: " .. file_result, vim.log.levels.ERROR)
return
end
vim.notify("Properties updated successfully", vim.log.levels.INFO)
-- Exit edit mode and refresh view
M.exit_edit_mode(buffer_id, editor_id)
-- Refresh the query results
if result and result.refresh_query then
buffer_manager.handle_refresh()
end
end
-- Cancel edit mode
function M.cancel_edit_mode(buffer_id, editor_id)
local editor_session = active_editors[editor_id]
if not editor_session then
return
end
if editor_session.modified then
vim.ui.select({"Discard changes", "Continue editing"}, {
prompt = "You have unsaved changes. What would you like to do?"
}, function(choice)
if choice == "Discard changes" then
M.exit_edit_mode(buffer_id, editor_id)
end
end)
else
M.exit_edit_mode(buffer_id, editor_id)
end
end
-- Exit edit mode
function M.exit_edit_mode(buffer_id, editor_id)
local editor_session = active_editors[editor_id]
if not editor_session then
return
end
-- Clean up editor session
active_editors[editor_id] = nil
-- Restore buffer to view mode
local config = buffer_manager.get_active_buffers()[buffer_id]
if config then
-- Regenerate original view content
local lines, _ = buffer_manager.generate_buffer_content(config.query_results, config.options)
vim.api.nvim_buf_set_lines(buffer_id, 0, -1, false, lines)
vim.api.nvim_buf_set_option(buffer_id, "modifiable", false)
vim.api.nvim_buf_set_option(buffer_id, "modified", false)
-- Restore original mappings
buffer_manager.setup_buffer_mappings(buffer_id, config.options)
end
utils.log("INFO", "Exited edit mode", {
document_id = editor_session.document_id,
file_path = editor_session.file_path
})
end
-- Parse properties from buffer
function M.parse_properties_from_buffer(buffer_id)
local lines = vim.api.nvim_buf_get_lines(buffer_id, 0, -1, false)
local properties = {}
-- Find property lines (skip header)
local in_properties = false
for _, line in ipairs(lines) do
if line:match("^Properties %(edit values") then
in_properties = true
continue
end
if in_properties and line:trim() == "" then
break
end
if in_properties then
local key, value = line:match("^(%s*[%w_%-%.]+%s*):%s*(.+)$")
if key and value then
local clean_key = key:trim():gsub(":$", "")
local clean_value = value:trim()
-- Parse value (handle quotes, numbers, booleans)
local parsed_value = M.parse_property_value(clean_value)
properties[clean_key] = parsed_value
end
end
end
return properties
end
-- Parse property value
function M.parse_property_value(value_string)
-- Handle quoted strings
local quoted = value_string:match('^"(.*)"$')
if quoted then
return quoted
end
quoted = value_string:match("^'(.*)'$")
if quoted then
return quoted
end
-- Handle numbers
local number = tonumber(value_string)
if number then
return number
end
-- Handle booleans
local lower = value_string:lower()
if lower == "true" then
return true
elseif lower == "false" then
return false
end
-- Handle arrays (simple format)
if value_string:match("^%[.+]$") then
local array_content = value_string:sub(2, -2)
local items = {}
for item in array_content:gsub("%s", ""):gmatch("[^,]+") do
table.insert(items, item:gsub("^['\"](.*)['\"]$", "%1"))
end
return items
end
-- Default to string
return value_string
end
-- Detect property changes
function M.detect_property_changes(original, edited)
local changes = {}
-- Find modified and added properties
for key, value in pairs(edited) do
if not original[key] then
table.insert(changes, {
type = "added",
key = key,
new_value = value
})
elseif original[key] ~= value then
table.insert(changes, {
type = "modified",
key = key,
old_value = original[key],
new_value = value
})
end
end
-- Find deleted properties
for key, value in pairs(original) do
if not edited[key] then
table.insert(changes, {
type = "deleted",
key = key,
old_value = value
})
end
end
return changes
end
-- Update document properties in database
function M.update_document_properties(document_id, changes)
local updated_count = 0
for _, change in ipairs(changes) do
if change.type == "deleted" then
-- Delete property
local ok, err = database.properties.delete_by_key(document_id, change.key)
if not ok then
return false, "Failed to delete property " .. change.key .. ": " .. err
end
else
-- Add or update property
local prop_data = {
id = utils.generate_id(),
document_id = document_id,
key = change.key,
value = tostring(change.new_value),
value_type = type(change.new_value),
created_at = os.time(),
updated_at = os.time()
}
-- Check if property already exists
local existing_prop, err = database.properties.get_by_key(document_id, change.key)
if err then
return false, "Failed to check existing property: " .. err
end
local ok
if existing_prop then
prop_data.id = existing_prop.id
ok, err = database.properties.update(prop_data)
else
ok, err = database.properties.create(prop_data)
end
if not ok then
return false, "Failed to update property " .. change.key .. ": " .. err
end
end
updated_count = updated_count + 1
end
return true, {
updated_count = updated_count,
refresh_query = true
}
end
-- Update YAML file
function M.update_yaml_file(file_path, properties)
-- Read current file
local content, err = utils.read_file(file_path)
if not content then
return false, "Failed to read file: " .. err
end
-- Parse YAML and content
local yaml_content, yaml_err = parser.yaml_parser.extract_yaml_header(content)
if not yaml_content then
return false, "Failed to extract YAML: " .. yaml_err
end
local body_content = parser.yaml_parser.remove_yaml_header(content)
-- Parse current YAML
local current_yaml, parse_err = parser.yaml_parser.parse_yaml(yaml_content)
if not current_yaml then
return false, "Failed to parse YAML: " .. parse_err
end
-- Update YAML data
for key, value in pairs(properties) do
current_yaml[key] = value
end
-- Generate new YAML header
local new_yaml_content = M.generate_yaml_content(current_yaml)
-- Combine with body content
local new_content = new_yaml_content .. "\n---\n" .. body_content
-- Write file
local write_ok, write_err = utils.write_file(file_path, new_content)
if not write_ok then
return false, "Failed to write file: " .. write_err
end
return true, "File updated successfully"
end
-- Generate YAML content from data
function M.generate_yaml_content(data)
local lines = {"---"}
-- Sort keys for consistent output
local sorted_keys = {}
for key, _ in pairs(data) do
table.insert(sorted_keys, key)
end
table.sort(sorted_keys)
for _, key in ipairs(sorted_keys) do
local value = data[key]
local line = M.format_yaml_value(key, value)
table.insert(lines, line)
end
return table.concat(lines, "\n")
end
-- Format YAML key-value pair
function M.format_yaml_value(key, value)
if type(value) == "string" then
return string.format('%s: "%s"', key, value)
elseif type(value) == "number" then
return string.format("%s: %s", key, tostring(value))
elseif type(value) == "boolean" then
return string.format("%s: %s", key, tostring(value))
elseif type(value) == "table" then
return string.format("%s: %s", key, vim.json.encode(value))
else
return string.format('%s: "%s"', key, tostring(value))
end
end
-- Add new property
function M.add_new_property(buffer_id, editor_id)
vim.ui.input({ prompt = "Property name: " }, function(property_name)
if property_name and property_name ~= "" then
vim.ui.input({ prompt = "Property value: " }, function(property_value)
if property_value and property_value ~= "" then
local editor_session = active_editors[editor_id]
if editor_session then
editor_session.current_properties[property_name] = property_value
editor_session.modified = true
M.update_buffer_for_editing(buffer_id, editor_id)
end
end
end)
end
end)
end
-- Delete property at cursor
function M.delete_property_at_cursor(buffer_id, editor_id)
local cursor = vim.api.nvim_win_get_cursor(0)
local line = vim.api.nvim_buf_get_lines(buffer_id, cursor[1] - 1, cursor[1], false)[1]
local key = line:match("^(%s*[%w_%-%.]+%s*):")
if key then
local clean_key = key:trim():gsub(":$", "")
local editor_session = active_editors[editor_id]
if editor_session and editor_session.current_properties[clean_key] then
editor_session.current_properties[clean_key] = nil
editor_session.modified = true
M.update_buffer_for_editing(buffer_id, editor_id)
vim.notify("Deleted property: " + clean_key, vim.log.levels.INFO)
end
end
end
-- Undo changes
function M.undo_changes(buffer_id, editor_id)
local editor_session = active_editors[editor_id]
if editor_session then
editor_session.current_properties = vim.deepcopy(editor_session.original_properties)
editor_session.modified = false
M.update_buffer_for_editing(buffer_id, editor_id)
vim.notify("Changes undone", vim.log.levels.INFO)
end
end
-- Get active editors
function M.get_active_editors()
local active = {}
for editor_id, session in pairs(active_editors) do
if vim.api.nvim_buf_is_valid(session.buffer_id) then
active[editor_id] = session
else
-- Clean up invalid sessions
active_editors[editor_id] = nil
end
end
return active
end
return M

480
lua/notex/ui/init.lua Normal file
View file

@ -0,0 +1,480 @@
-- UI coordination module
local M = {}
local buffer_manager = require('notex.ui.buffer')
local view = require('notex.ui.view')
local editor = require('notex.ui.editor')
local query_engine = require('notex.query')
local utils = require('notex.utils')
-- UI state
local ui_state = {
active_views = {},
default_options = {
max_width = 120,
max_height = 30,
show_help = true,
border = "rounded"
}
}
-- Show query results
function M.show_query_results(query_results, options)
options = options or {}
options = vim.tbl_deep_extend("force", ui_state.default_options, options)
-- Validate query results
if not query_results.success then
M.show_error("Query Error", query_results.errors, options)
return nil, query_results.errors
end
-- Create view
local view_config = view.create_query_view(query_results, options)
if not view_config then
return nil, "Failed to create query view"
end
-- Store active view
ui_state.active_views[view_config.buffer_id] = view_config
-- Setup auto-cleanup
M.setup_view_cleanup(view_config)
utils.log("INFO", "Created query view", {
buffer_id = view_config.buffer_id,
document_count = #query_results.documents,
view_type = options.view_type or "table"
})
return view_config
end
-- Show error message
function M.show_error(title, errors, options)
options = options or {}
local error_lines = {title, string.rep("=", #title), ""}
if type(errors) == "string" then
table.insert(error_lines, errors)
elseif type(errors) == "table" then
for _, error in ipairs(errors) do
table.insert(error_lines, "" .. error)
end
end
table.insert(error_lines, "")
table.insert(error_lines, "Press any key to close")
local buffer = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(buffer, 0, -1, false, error_lines)
vim.api.nvim_buf_set_option(buffer, "filetype", "text")
vim.api.nvim_buf_set_name(buffer, "notex://error")
local window = vim.api.nvim_open_win(buffer, true, {
relative = "editor",
width = math.min(80, vim.api.nvim_get_option_value("columns", {})),
height = math.min(20, vim.api.nvim_get_option_value("lines", {})),
row = math.floor((vim.api.nvim_get_option_value("lines", {}) - 20) / 2),
col = math.floor((vim.api.nvim_get_option_value("columns", {}) - 80) / 2),
border = "rounded",
style = "minimal",
title = " Error "
})
-- Close on any key
vim.api.nvim_create_autocmd("CursorMoved,WinLeave", {
buffer = buffer,
once = true,
callback = function()
vim.api.nvim_win_close(window, true)
end
})
return {
buffer_id = buffer,
window_id = window,
type = "error"
}
end
-- Show document details
function M.show_document_details(document_id, options)
options = options or {}
-- Get document details
local indexer = require('notex.index')
local doc_details, err = indexer.get_document_details(document_id)
if not doc_details then
M.show_error("Document Error", {err or "Failed to get document details"})
return nil
end
-- Create detail view
local buffer = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_option(buffer, "filetype", "yaml")
vim.api.nvim_buf_set_name(buffer, "notex://document-details")
-- Generate detail content
local lines = M.generate_document_details(doc_details)
vim.api.nvim_buf_set_lines(buffer, 0, -1, false, lines)
-- Create window
local window = vim.api.nvim_open_win(buffer, true, {
relative = "editor",
width = math.min(100, vim.api.nvim_get_option_value("columns", {})),
height = math.min(40, vim.api.nvim_get_option_value("lines", {})),
row = 1,
col = 1,
border = "rounded",
style = "minimal",
title = " Document Details "
})
-- Setup mappings
local mappings = {
["<CR>"] = {
callback = function()
vim.cmd('edit ' .. doc_details.document.file_path)
vim.api.nvim_win_close(window, true)
end,
description = "Open document"
},
["e"] = {
callback = function()
editor.start_edit_mode(buffer, 1, 1)
end,
description = "Edit properties"
},
["q"] = {
callback = function()
vim.api.nvim_win_close(window, true)
end,
description = "Close"
}
}
for key, action in pairs(mappings) do
vim.keymap.set("n", key, action.callback, {
buffer = buffer,
noremap = true,
silent = true
})
end
return {
buffer_id = buffer,
window_id = window,
type = "document_details",
document_id = document_id
}
end
-- Generate document details content
function M.generate_document_details(doc_details)
local lines = {}
-- Header
table.insert(lines, "Document Details")
table.insert(lines, string.rep("=", 50))
table.insert(lines, "")
-- File information
table.insert(lines, "## File Information")
table.insert(lines, string.format("Path: %s", doc_details.document.file_path))
table.insert(lines, string.format("Created: %s", os.date("%Y-%m-%d %H:%M:%S", doc_details.document.created_at)))
table.insert(lines, string.format("Modified: %s", os.date("%Y-%m-%d %H:%M:%S", doc_details.document.updated_at)))
table.insert(lines, string.format("Content Hash: %s", doc_details.document.content_hash))
table.insert(lines, string.format("File Exists: %s", doc_details.file_exists and "Yes" or "No"))
table.insert(lines, "")
-- Properties
table.insert(lines, "## Properties")
if doc_details.properties and #doc_details.properties > 0 then
for _, prop in ipairs(doc_details.properties) do
local value_str = tostring(prop.value)
if #value_str > 100 then
value_str = value_str:sub(1, 97) .. "..."
end
table.insert(lines, string.format("%s: %s (%s)", prop.key, value_str, prop.value_type))
end
else
table.insert(lines, "No properties found")
end
table.insert(lines, "")
-- Parse information
if doc_details.parse_result and doc_details.parse_result.success then
table.insert(lines, "## Analysis")
local analysis = doc_details.parse_result.markdown_analysis
table.insert(lines, string.format("Word Count: %d", analysis.word_count))
table.insert(lines, string.format("Character Count: %d", analysis.character_count))
table.insert(lines, string.format("Line Count: %d", analysis.line_count))
table.insert(lines, string.format("Reading Time: %d minutes", analysis.reading_time_minutes))
if #analysis.headings > 0 then
table.insert(lines, "")
table.insert(lines, "### Headings")
for _, heading in ipairs(analysis.headings) do
local indent = string.rep(" ", heading.level)
table.insert(lines, indent .. heading.title)
end
end
if #analysis.links > 0 then
table.insert(lines, "")
table.insert(lines, "### Links")
for i, link in ipairs(analysis.links) do
if i <= 5 then -- Limit to 5 links
table.insert(lines, string.format("• %s → %s", link.text, link.url))
end
end
if #analysis.links > 5 then
table.insert(lines, string.format("... and %d more", #analysis.links - 5))
end
end
end
table.insert(lines, "")
table.insert(lines, "Press <Enter> to open document, e to edit, q to close")
return lines
end
-- Switch view type
function M.switch_view_type(new_view_type)
local current_buffer = vim.api.nvim_get_current_buf()
local success, result = view.switch_view_type(current_buffer, new_view_type)
if success then
vim.notify("Switched to " .. new_view_type .. " view", vim.log.levels.INFO)
else
vim.notify("Failed to switch view: " .. result, vim.log.levels.ERROR)
end
end
-- Show view type menu
function M.show_view_type_menu()
local view_types = view.get_available_view_types()
local choices = {}
for _, view_type in ipairs(view_types) do
table.insert(choices, string.format("%s %s - %s", view_type.icon, view_type.name, view_type.description))
end
vim.ui.select(choices, {
prompt = "Select view type:",
format_item = function(item)
return item
end
}, function(choice)
if choice then
-- Extract view type name from choice
local view_type = choice:match("%s(%w+)%s-") or choice:match("(%w+)%s-")
if view_type then
M.switch_view_type(view_type)
end
end
end)
end
-- Export view
function M.export_view(format)
local current_buffer = vim.api.nvim_get_current_buf()
local success, result = view.export_view(current_buffer, format)
if success then
-- Ask user for file location
local default_filename = "notex_export." .. format
vim.ui.input({ prompt = "Export to file: ", default = default_filename }, function(filename)
if filename and filename ~= "" then
local write_ok, write_err = utils.write_file(filename, result)
if write_ok then
vim.notify("Exported to " .. filename, vim.log.levels.INFO)
else
vim.notify("Failed to export: " .. write_err, vim.log.levels.ERROR)
end
end
end)
else
vim.notify("Failed to export: " .. result, vim.log.levels.ERROR)
end
end
-- Show export menu
function M.show_export_menu()
local formats = {
{name = "Markdown", extension = "md", description = "Markdown format"},
{name = "CSV", extension = "csv", description = "Comma-separated values"},
{name = "JSON", extension = "json", description = "JSON format"}
}
local choices = {}
for _, format in ipairs(formats) do
table.insert(choices, string.format("%s (%s) - %s", format.name, format.extension, format.description))
end
vim.ui.select(choices, {
prompt = "Select export format:"
}, function(choice)
if choice then
local format_name = choice:match("(%w+)%s+%(")
if format_name then
M.export_view(format_name:lower())
end
end
end)
end
-- Show query prompt
function M.show_query_prompt(initial_query)
initial_query = initial_query or ""
vim.ui.input({
prompt = "Query: ",
default = initial_query,
completion = "customlist,require('notex.ui').get_query_completions"
}, function(query_string)
if query_string and query_string ~= "" then
M.execute_query_and_show_results(query_string)
end
end)
end
-- Execute query and show results
function M.execute_query_and_show_results(query_string, options)
options = options or {}
vim.notify("Executing query...", vim.log.levels.INFO)
-- Execute query
local result = query_engine.execute_query(query_string, options)
if result.success then
-- Show results
local view_config = M.show_query_results(result, options)
if view_config then
utils.log("INFO", "Query executed successfully", {
document_count = #result.documents,
execution_time_ms = result.execution_time_ms
})
end
else
M.show_error("Query Failed", result.errors, options)
end
end
-- Get query completions
function M.get_query_completions()
local suggestions = query_engine.get_suggestions("", 0)
local completions = {}
-- Property suggestions
for _, prop in ipairs(suggestions.properties or {}) do
table.insert(completions, prop .. ":")
end
-- Value suggestions for common properties
for prop, values in pairs(suggestions.values or {}) do
for _, value in ipairs(values) do
table.insert(completions, prop .. ' = "' .. value .. '"')
end
end
-- Operator suggestions
for _, op in ipairs(suggestions.operators or {}) do
table.insert(completions, "WHERE " .. op)
end
table.sort(completions)
return completions
end
-- Setup view cleanup
function M.setup_view_cleanup(view_config)
local group = vim.api.nvim_create_augroup("NotexViewCleanup_" .. view_config.buffer_id, {clear = true})
vim.api.nvim_create_autocmd({"BufLeave", "WinLeave"}, {
buffer = view_config.buffer_id,
once = true,
callback = function()
-- Give user time to interact, then cleanup
vim.defer_fn(function()
if vim.api.nvim_buf_is_valid(view_config.buffer_id) then
ui_state.active_views[view_config.buffer_id] = nil
end
end, 1000)
end
})
end
-- Get UI status
function M.get_ui_status()
local active_views = buffer_manager.get_active_buffers()
local active_editors = editor.get_active_editors()
return {
active_views_count = vim.tbl_count(active_views),
active_editors_count = vim.tbl_count(active_editors),
total_windows = vim.tbl_count(vim.api.nvim_list_wins()),
current_buffer = vim.api.nvim_get_current_buf(),
current_window = vim.api.nvim_get_current_win()
}
end
-- Cleanup all UI components
function M.cleanup_all()
-- Close all notex buffers
local buffers = vim.api.nvim_list_bufs()
for _, buf in ipairs(buffers) do
local buf_name = vim.api.nvim_buf_get_name(buf)
if buf_name:match("^notex://") then
if vim.api.nvim_buf_is_valid(buf) then
vim.api.nvim_buf_delete(buf, {force = true})
end
end
end
-- Clear state
ui_state.active_views = {}
buffer_manager.cleanup_buffers()
utils.log("INFO", "Cleaned up all UI components")
end
-- Initialize UI system
function M.init()
-- Set up global keymaps if not already set
local global_keymaps = {
["<leader>nq"] = {
callback = function() M.show_query_prompt() end,
description = "New query"
},
["<leader>nv"] = {
callback = function() M.show_view_type_menu() end,
description = "Switch view type"
},
["<leader>ne"] = {
callback = function() M.show_export_menu() end,
description = "Export view"
}
}
for key, action in pairs(global_keymaps) do
if not vim.fn.hasmapto(key, "n") then
vim.keymap.set("n", key, action.callback, {
noremap = true,
silent = true,
desc = action.description
})
end
end
utils.log("INFO", "UI system initialized")
return true, "UI system initialized successfully"
end
return M

525
lua/notex/ui/view.lua Normal file
View file

@ -0,0 +1,525 @@
-- Query result visualization module
local M = {}
local buffer_manager = require('notex.ui.buffer')
local utils = require('notex.utils')
-- View configurations
local view_configs = {}
-- Create query view
function M.create_query_view(query_results, options)
options = options or {}
local view_type = options.view_type or "table"
local view_config = {
view_type = view_type,
query_results = query_results,
options = options,
created_at = os.time()
}
-- Create buffer based on view type
if view_type == "table" then
return M.create_table_view(query_results, options)
elseif view_type == "cards" then
return M.create_cards_view(query_results, options)
elseif view_type == "list" then
return M.create_list_view(query_results, options)
elseif view_type == "tree" then
return M.create_tree_view(query_results, options)
else
return M.create_table_view(query_results, options)
end
end
-- Create table view
function M.create_table_view(query_results, options)
local table_options = vim.tbl_deep_extend("force", options, {
name = "notex://table-view",
view_type = "table",
include_properties = M.get_table_properties(query_results),
max_width = 120,
show_help = true
})
return buffer_manager.create_query_buffer(query_results, table_options)
end
-- Create cards view
function M.create_cards_view(query_results, options)
local cards_options = vim.tbl_deep_extend("force", options, {
name = "notex://cards-view",
view_type = "cards",
show_help = true,
wrap = true
})
local buffer_id = vim.api.nvim_create_buf(false, true)
buffer_manager.setup_buffer_options(buffer_id, cards_options)
local lines = M.generate_cards_content(query_results, cards_options)
vim.api.nvim_buf_set_lines(buffer_id, 0, -1, false, lines)
local window_id = buffer_manager.create_query_window(buffer_id, cards_options)
local config = {
buffer_id = buffer_id,
window_id = window_id,
query_results = query_results,
options = cards_options,
created_at = os.time(),
mappings = buffer_manager.setup_buffer_mappings(buffer_id, cards_options)
}
return config
end
-- Create list view
function M.create_list_view(query_results, options)
local list_options = vim.tbl_deep_extend("force", options, {
name = "notex://list-view",
view_type = "list",
show_help = true
})
local buffer_id = vim.api.nvim_create_buf(false, true)
buffer_manager.setup_buffer_options(buffer_id, list_options)
local lines = M.generate_list_content(query_results, list_options)
vim.api.nvim_buf_set_lines(buffer_id, 0, -1, false, lines)
local window_id = buffer_manager.create_query_window(buffer_id, list_options)
local config = {
buffer_id = buffer_id,
window_id = window_id,
query_results = query_results,
options = list_options,
created_at = os.time(),
mappings = buffer_manager.setup_buffer_mappings(buffer_id, list_options)
}
return config
end
-- Create tree view
function M.create_tree_view(query_results, options)
local tree_options = vim.tbl_deep_extend("force", options, {
name = "notex://tree-view",
view_type = "tree",
group_by = options.group_by or "status",
show_help = true
})
local buffer_id = vim.api.nvim_create_buf(false, true)
buffer_manager.setup_buffer_options(buffer_id, tree_options)
local lines = M.generate_tree_content(query_results, tree_options)
vim.api.nvim_buf_set_lines(buffer_id, 0, -1, false, lines)
local window_id = buffer_manager.create_query_window(buffer_id, tree_options)
local config = {
buffer_id = buffer_id,
window_id = window_id,
query_results = query_results,
options = tree_options,
created_at = os.time(),
mappings = buffer_manager.setup_buffer_mappings(buffer_id, tree_options)
}
return config
end
-- Get table properties
function M.get_table_properties(query_results)
if not query_results.documents or #query_results.documents == 0 then
return {"title", "status", "priority"}
end
-- Find most common properties
local property_counts = {}
for _, doc in ipairs(query_results.documents) do
if doc.properties then
for prop, _ in pairs(doc.properties) do
property_counts[prop] = (property_counts[prop] or 0) + 1
end
end
end
-- Sort by frequency
local sorted_props = {}
for prop, count in pairs(property_counts) do
table.insert(sorted_props, {property = prop, count = count})
end
table.sort(sorted_props, function(a, b) return a.count > b.count end)
-- Return top properties
local result = {}
for i, item in ipairs(sorted_props) do
if i > 6 then break end -- Limit to 6 columns
table.insert(result, item.property)
end
return result
end
-- Generate cards content
function M.generate_cards_content(query_results, options)
local lines = {}
-- Header
table.insert(lines, "Query Results - Card View")
table.insert(lines, string.rep("=", 50))
table.insert(lines, "")
-- Query info
if query_results.query_string then
table.insert(lines, "Query: " .. query_results.query_string)
table.insert(lines, "")
end
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, "")
-- Document cards
if query_results.documents and #query_results.documents > 0 then
for i, doc in ipairs(query_results.documents) do
lines = M.add_document_card(lines, doc, i, options)
table.insert(lines, "")
end
else
table.insert(lines, "No documents found.")
table.insert(lines, "")
end
-- Help
lines = buffer_manager.add_help_section(lines, options)
return lines
end
-- Add document card
function M.add_document_card(lines, doc, index, options)
local max_width = options.max_width or 80
table.insert(lines, string.format("Document %d: %s", index, doc.properties and doc.properties.title or "Untitled"))
table.insert(lines, string.rep("-", max_width))
-- File path
table.insert(lines, "Path: " .. doc.file_path)
-- Properties
if doc.properties then
local sorted_props = {}
for key, value in pairs(doc.properties) do
table.insert(sorted_props, {key = key, value = value})
end
table.sort(sorted_props, function(a, b) return a.key < b.key end)
for _, prop in ipairs(sorted_props) do
if prop.key ~= "title" then
local formatted = string.format(" %s: %s", prop.key, tostring(prop.value))
if #formatted > max_width - 2 then
formatted = formatted:sub(1, max_width - 5) .. "..."
end
table.insert(lines, formatted)
end
end
end
-- Metadata
table.insert(lines, string.format(" Modified: %s", os.date("%Y-%m-%d %H:%M", doc.updated_at)))
return lines
end
-- Generate list content
function M.generate_list_content(query_results, options)
local lines = {}
-- Header
table.insert(lines, "Query Results - List View")
table.insert(lines, string.rep("=", 50))
table.insert(lines, "")
-- Query info
table.insert(lines, string.format("%d documents (%.2fms)",
query_results.total_count or 0,
query_results.execution_time_ms or 0))
table.insert(lines, "")
-- Document list
if query_results.documents and #query_results.documents > 0 then
for i, doc in ipairs(query_results.documents) do
local title = doc.properties and doc.properties.title or vim.fn.fnamemodify(doc.file_path, ":t")
local status = doc.properties and doc.properties.status or ""
local priority = doc.properties and doc.properties.priority or ""
local line = string.format("%3d. %-40s %-12s %-8s", i, M.truncate_string(title, 40), status, priority)
table.insert(lines, line)
end
else
table.insert(lines, "No documents found.")
end
table.insert(lines, "")
-- Help
lines = buffer_manager.add_help_section(lines, options)
return lines
end
-- Generate tree content
function M.generate_tree_content(query_results, options)
local lines = {}
local group_by = options.group_by or "status"
-- Header
table.insert(lines, string.format("Query Results - Tree View (Grouped by %s)", group_by))
table.insert(lines, string.rep("=", 50))
table.insert(lines, "")
-- Group documents
local groups = M.group_documents(query_results.documents, group_by)
-- Create tree structure
for group_name, group_docs in pairs(groups) do
table.insert(lines, string.format("▼ %s (%d)", group_name, #group_docs))
for i, doc in ipairs(group_docs) do
local title = doc.properties and doc.properties.title or vim.fn.fnamemodify(doc.file_path, ":t")
local line = string.format(" ├─ %s", title)
if i == #group_docs then
line = string.format(" └─ %s", title)
end
table.insert(lines, line)
end
table.insert(lines, "")
end
-- Help
lines = buffer_manager.add_help_section(lines, options)
return lines
end
-- Group documents by property
function M.group_documents(documents, group_by)
local groups = {}
for _, doc in ipairs(documents) do
local group_value = "Unknown"
if doc.properties and doc.properties[group_by] then
group_value = tostring(doc.properties[group_by])
end
if not groups[group_value] then
groups[group_value] = {}
end
table.insert(groups[group_value], doc)
end
-- Sort groups
local sorted_groups = {}
for group_name, group_docs in pairs(groups) do
table.insert(sorted_groups, {name = group_name, docs = group_docs})
end
table.sort(sorted_groups, function(a, b) return a.name < b.name end)
local result = {}
for _, group in ipairs(sorted_groups) do
result[group.name] = group.docs
end
return result
end
-- Truncate string
function M.truncate_string(str, max_length)
if #str <= max_length then
return str
end
return str:sub(1, max_length - 3) .. "..."
end
-- Switch view type
function M.switch_view_type(buffer_id, new_view_type)
local config = require('notex.ui.buffer').get_active_buffers()[buffer_id]
if not config then
return false, "Buffer not found"
end
-- Close current view
if config.window_id then
vim.api.nvim_win_close(config.window_id, true)
end
-- Create new view
local new_options = vim.tbl_deep_extend("force", config.options, {
view_type = new_view_type
})
local new_config = M.create_query_view(config.query_results, new_options)
if new_config then
return true, "View switched to " .. new_view_type
else
return false, "Failed to create new view"
end
end
-- Get available view types
function M.get_available_view_types()
return {
{
name = "table",
description = "Tabular view with sortable columns",
icon = ""
},
{
name = "cards",
description = "Card-based view with detailed information",
icon = "📄"
},
{
name = "list",
description = "Compact list view",
icon = "📋"
},
{
name = "tree",
description = "Hierarchical view grouped by properties",
icon = "🌳"
}
}
end
-- Export view to different formats
function M.export_view(buffer_id, format)
local config = require('notex.ui.buffer').get_active_buffers()[buffer_id]
if not config then
return false, "Buffer not found"
end
format = format or "markdown"
if format == "markdown" then
return M.export_to_markdown(config.query_results, config.options)
elseif format == "csv" then
return M.export_to_csv(config.query_results, config.options)
elseif format == "json" then
return M.export_to_json(config.query_results, config.options)
else
return false, "Unsupported export format: " .. format
end
end
-- Export to markdown
function M.export_to_markdown(query_results, options)
local lines = {}
table.insert(lines, "# Query Results")
table.insert(lines, "")
if query_results.query_string then
table.insert(lines, "## Query")
table.insert(lines, "```")
table.insert(lines, query_results.query_string)
table.insert(lines, "```")
table.insert(lines, "")
end
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, "")
if query_results.documents and #query_results.documents > 0 then
table.insert(lines, "## Documents")
table.insert(lines, "")
for i, doc in ipairs(query_results.documents) do
table.insert(lines, string.format("### %d. %s", i, doc.properties and doc.properties.title or "Untitled"))
table.insert(lines, "")
table.insert(lines, "**File:** `" .. doc.file_path .. "`")
table.insert(lines, "")
if doc.properties then
table.insert(lines, "**Properties:**")
for key, value in pairs(doc.properties) do
table.insert(lines, string.format("- **%s:** %s", key, tostring(value)))
end
table.insert(lines, "")
end
end
end
return true, table.concat(lines, "\n")
end
-- Export to CSV
function M.export_to_csv(query_results, options)
local lines = {}
-- Header
local headers = {"#", "File", "Title", "Status", "Priority", "Created", "Modified"}
table.insert(lines, table.concat(headers, ","))
-- Data rows
if query_results.documents then
for i, doc in ipairs(query_results.documents) do
local row = {
i,
doc.file_path,
doc.properties and doc.properties.title or "",
doc.properties and doc.properties.status or "",
doc.properties and doc.properties.priority or "",
doc.created_at and os.date("%Y-%m-%d", doc.created_at) or "",
doc.updated_at and os.date("%Y-%m-%d", doc.updated_at) or ""
}
-- Escape CSV values
for j, value in ipairs(row) do
if value:find("[,\"]") then
row[j] = '"' .. value:gsub('"', '""') .. '"'
end
end
table.insert(lines, table.concat(row, ","))
end
end
return true, table.concat(lines, "\n")
end
-- Export to JSON
function M.export_to_json(query_results, options)
local export_data = {
query = query_results.query_string,
total_count = query_results.total_count,
execution_time_ms = query_results.execution_time_ms,
documents = query_results.documents,
exported_at = os.time(),
exported_by = "notex.nvim"
}
return true, vim.json.encode(export_data)
end
return M