-- 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 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, " - Save changes") table.insert(lines, " - 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", "", function() M.save_and_exit_edit_mode(buffer_id, editor_id) end, opts) vim.keymap.set("i", "", function() M.save_and_exit_edit_mode(buffer_id, editor_id) end, opts) -- Cancel editing vim.keymap.set("n", "", function() M.cancel_edit_mode(buffer_id, editor_id) end, opts) vim.keymap.set("i", "", 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