-- Notex: Relational Document System for Neovim local M = {} -- Plugin configuration local config = { database_path = nil, auto_index = true, index_on_startup = false, max_file_size = 10 * 1024 * 1024, -- 10MB default_view_type = "table", performance = { max_query_time = 5000, -- 5 seconds cache_size = 100, enable_caching = true }, ui = { border = "rounded", max_width = 120, max_height = 30, show_help = true } } -- Plugin state local state = { initialized = false, database = nil, ui = nil, settings = {} } -- Initialize plugin function M.setup(user_config) -- Merge user configuration M.settings = vim.tbl_deep_extend("force", config, user_config or {}) -- Validate configuration local valid, errors = M.validate_config(M.settings) if not valid then vim.notify("Notex configuration error: " .. table.concat(errors, ", "), vim.log.levels.ERROR) return false end -- Initialize modules local ok, err = M.initialize_modules() if not ok then vim.notify("Failed to initialize Notex: " .. err, vim.log.levels.ERROR) return false end -- Initialize caching system if enabled if M.settings.performance.enable_caching then local cache = require('notex.utils.cache') cache.init({ memory = { max_size = M.settings.performance.cache_size, enabled = true }, lru = { max_size = math.floor(M.settings.performance.cache_size / 2), enabled = true }, timed = { default_ttl = 300, -- 5 minutes enabled = true } }) end -- Setup autocommands M.setup_autocommands() -- Setup keymaps M.setup_keymaps() -- Auto-index on startup if enabled if M.settings.index_on_startup then vim.defer_fn(function() M.auto_index_current_workspace() end, 1000) end state.initialized = true vim.notify("Notex initialized successfully", vim.log.levels.INFO) return true end -- Validate configuration function M.validate_config(settings) local errors = {} -- Validate performance settings if settings.performance then if settings.performance.max_query_time and settings.performance.max_query_time < 100 then table.insert(errors, "max_query_time must be at least 100ms") end if settings.performance.cache_size and settings.performance.cache_size < 10 then table.insert(errors, "cache_size must be at least 10") end end -- Validate UI settings if settings.ui then if settings.ui.max_width and settings.ui.max_width < 40 then table.insert(errors, "max_width must be at least 40 characters") end if settings.ui.max_height and settings.ui.max_height < 10 then table.insert(errors, "max_height must be at least 10 characters") end end return #errors == 0, errors end -- Initialize modules function M.initialize_modules() -- Initialize database local database = require('notex.database.init') local db_path = M.settings.database_path or vim.fn.stdpath('data') .. '/notex/notex.db' local ok, err = database.init(db_path) if not ok then return false, err end state.database = database -- Initialize migrations local migrations = require('notex.database.migrations') ok, err = migrations.init() if not ok then return false, err end -- Initialize UI local ui = require('notex.ui') ok, err = ui.init() if not ok then return false, err end state.ui = ui -- Initialize query engine local query_engine = require('notex.query') ok, err = query_engine.init(db_path) if not ok then return false, err end return true, "Modules initialized successfully" end -- Setup autocommands function M.setup_autocommands() local group = vim.api.nvim_create_augroup("Notex", {clear = true}) -- Auto-index markdown files when saved (if enabled) if M.settings.auto_index then vim.api.nvim_create_autocmd("BufWritePost", { pattern = "*.md", callback = function() M.auto_index_file(vim.fn.expand('%:p')) end }) end -- Clean up on exit vim.api.nvim_create_autocmd("VimLeavePre", { callback = function() M.cleanup() end }) -- Detect query blocks and show hover vim.api.nvim_create_autocmd({"CursorHold", "CursorHoldI"}, { pattern = "*.md", callback = function() M.check_query_block_under_cursor() end }) end -- Setup keymaps function M.setup_keymaps() -- Global keymaps local keymaps = { ["nq"] = {callback = M.show_query_prompt, desc = "New query"}, ["nr"] = {callback = M.show_recent_queries, desc = "Recent queries"}, ["ns"] = {callback = M.show_saved_queries, desc = "Saved queries"}, ["ni"] = {callback = M.index_workspace, desc = "Index workspace"}, ["ns"] = {callback = M.show_index_status, desc = "Index status"}, ["nc"] = {callback = M.cleanup_database, desc = "Cleanup database"}, ["nv"] = {callback = M.switch_view_type, desc = "Switch view type"}, ["ne"] = {callback = M.show_export_menu, desc = "Export view"} } for key, mapping in pairs(keymaps) do if not vim.fn.hasmapto(key, "n") then vim.keymap.set("n", key, mapping.callback, { noremap = true, silent = true, desc = mapping.desc }) end end -- Buffer-local keymaps for query blocks vim.api.nvim_create_autocmd("FileType", { pattern = "markdown", callback = function() vim.keymap.set("n", "K", M.show_query_under_cursor, { buffer = 0, noremap = true, silent = true, desc = "Show query" }) end }) end -- Show query prompt function M.show_query_prompt(initial_query) local ui = state.ui or require('notex.ui') ui.show_query_prompt(initial_query) end -- Show recent queries function M.show_recent_queries() local query_engine = require('notex.query') local result = query_engine.get_query_statistics() if result.success and result.statistics.recent_queries then local ui = state.ui or require('notex.ui') -- Create a simple UI to show recent queries local recent_queries = result.statistics.recent_queries if #recent_queries > 0 then local choices = {} for _, query in ipairs(recent_queries) do table.insert(choices, string.format("%s (%d uses)", query.name, query.use_count)) end vim.ui.select(choices, { prompt = "Select recent query:" }, function(choice) if choice then local query_name = choice:match("^(%s+) %(") if query_name then ui.execute_query_and_show_results(query_name:trim()) end end end) else vim.notify("No recent queries found", vim.log.levels.INFO) end else vim.notify("Failed to get query statistics", vim.log.levels.ERROR) end end -- Show saved queries function M.show_saved_queries() local query_engine = require('notex.query') local result = query_engine.list_saved_queries() if result.success then local choices = {} for _, query in ipairs(result.queries) do table.insert(choices, query.name) end vim.ui.select(choices, { prompt = "Select saved query:" }, function(choice) if choice then local ui = state.ui or require('notex.ui') ui.execute_query_and_show_results(choice) end end) else vim.notify("Failed to get saved queries: " .. result.error, vim.log.levels.ERROR) end end -- Execute query and show results function M.execute_query_and_show_results(query_string, options) local utils = require('notex.utils') -- Use caching if enabled local result if M.settings.performance.enable_caching then local cache_key = "query:" .. vim.fn.sha256(query_string) result = utils.cache_get_or_set(cache_key, function() local query_engine = require('notex.query') return query_engine.execute_query(query_string, options) end, "lru", 300) -- Cache for 5 minutes else local query_engine = require('notex.query') result = query_engine.execute_query(query_string, options) end if result.success then local ui = state.ui or require('notex.ui') ui.show_query_results(result, M.settings.ui) else local ui = state.ui or require('notex.ui') ui.show_error("Query Error", result.errors, M.settings.ui) end end -- Index workspace function M.index_workspace() local workspace_dir = vim.fn.getcwd() M.index_directory(workspace_dir, { force_reindex = vim.fn.confirm("Force reindex? " , "&Yes\n&No", 2) == 1 }) end -- Index directory function M.index_directory(directory, options) options = options or {} vim.notify("Indexing documents in " .. directory, vim.log.levels.INFO) local indexer = require('notex.index') local result = indexer.index_documents(directory, options) if result.success then vim.notify(string.format("Indexing complete: %d documents processed", result.stats.indexed), vim.log.levels.INFO) M.log_indexing_result(result) else vim.notify("Indexing failed: " .. table.concat(result.errors, ", "), vim.log.levels.ERROR) end end -- Auto-index current workspace function M.auto_index_current_workspace() local workspace_dir = vim.fn.getcwd() M.index_directory(workspace_dir) end -- Auto-index single file function M.auto_index_file(file_path) local indexer = require('notex.index') -- Validate file local validation = require('notex.index.scanner').validate_markdown_file(file_path) if not validation.valid then return end local result = indexer.update_document(file_path) if result.success then vim.notify("Indexed: " .. file_path, vim.log.levels.INFO) end end -- Show index status function M.show_index_status() local indexer = require('notex.index') local stats = indexer.get_statistics() local lines = { "Notex Index Status", string.rep("=", 50), "", string.format("Documents indexed: %d", stats.document_count or 0), string.format("Properties: %d", stats.property_count or 0), string.format("Unique properties: %d", stats.unique_properties or 0), "", "Database:", string.format(" Size: %s bytes", stats.database.size_bytes or 0), string.format(" WAL mode: %s", stats.database.wal_mode and "Yes" or "No"), "", "Press any key to close" } local buffer = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(buffer, 0, -1, false, lines) vim.api.nvim_buf_set_option(buffer, "filetype", "text") local window = vim.api.nvim_open_win(buffer, 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 = " Index Status " }) vim.api.nvim_create_autocmd("CursorMoved,WinLeave", { buffer = buffer, once = true, callback = function() vim.api.nvim_win_close(window, true) end }) end -- Switch view type function M.switch_view_type() local ui = state.ui or require('notex.ui') ui.show_view_type_menu() end -- Show export menu function M.show_export_menu() local ui = state.ui or require('notex.ui') ui.show_export_menu() end -- Check query block under cursor function M.check_query_block_under_cursor() local line = vim.api.nvim_get_current_line() local cursor = vim.api.nvim_win_get_cursor(0) -- Check if cursor is on a query block if line:match("```notex%-query") or line:match("FROM") or line:match("WHERE") or line:match("ORDER BY") then -- Could be inside a query block, check surrounding context M.show_query_under_cursor() end end -- Show query under cursor function M.show_query_under_cursor() local line_start = math.max(1, vim.api.nvim_win_get_cursor(0)[1] - 5) local line_end = math.min(vim.api.nvim_buf_line_count(0), vim.api.nvim_win_get_cursor(0)[1] + 5) local lines = vim.api.nvim_buf_get_lines(0, line_start - 1, line_end - line_start + 1, false) local query_block = M.extract_query_block(lines) if query_block then M.execute_query_and_show_results(query_block) end end -- Extract query block from lines function M.extract_query_block(lines) local in_query_block = false local query_lines = {} for _, line in ipairs(lines) do if line:match("```notex%-query") then in_query_block = true elseif line:match("```") and in_query_block then break elseif in_query_block then table.insert(query_lines, line) end end if #query_lines > 0 then return table.concat(query_lines, "\n") end return nil end -- Cleanup database function M.cleanup_database() local indexer = require('notex.index') local cleanup_result = indexer.cleanup_index() vim.notify(string.format("Cleanup completed: %d orphans removed, %d missing files removed", cleanup_result.removed_orphans, cleanup_result.removed_missing), vim.log.levels.INFO) end -- Log indexing result function M.log_indexing_result(result) if result.stats.scanned > 0 then local success_rate = (result.stats.indexed / result.stats.scanned) * 100 vim.notify(string.format("Indexing success rate: %.1f%%", success_rate), vim.log.levels.INFO) end if #result.errors > 0 then vim.notify("Indexing had " .. #result.errors .. " errors", vim.log.levels.WARN) end end -- Cleanup function M.cleanup() if state.ui then state.ui.cleanup_all() end if state.database then state.database.close() end state.initialized = false end -- Get plugin status function M.status() return { initialized = state.initialized, database = state.database and state.database.status() or nil, settings = M.settings } end -- Health check function M.health_check() local health = { ok = true, checks = {} } -- Check database if state.database then local db_status = state.database.status() table.insert(health.checks, { name = "Database", status = db_status.initialized and "ok" or "error", message = db_status.initialized and "Database initialized" or "Database not initialized" }) else table.insert(health.checks, { name = "Database", status = "error", message = "Database not initialized" }) health.ok = false end -- Check modules local modules = {"database", "parser", "query", "ui", "index"} for _, module_name in ipairs(modules) do local ok, module = pcall(require, "notex." .. module_name) table.insert(health.checks, { name = "Module: " .. module_name, status = ok and "ok" or "error", message = ok and "Module loaded" or "Module failed to load" }) if not ok then health.ok = false end end return health end -- Export main API return M