notex.nvim/lua/notex/ui/editor.lua

572 lines
No EOL
16 KiB
Lua

-- 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