551 lines
15 KiB
Lua
551 lines
15 KiB
Lua
|
-- 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 = {
|
||
|
["<leader>nq"] = {callback = M.show_query_prompt, desc = "New query"},
|
||
|
["<leader>nr"] = {callback = M.show_recent_queries, desc = "Recent queries"},
|
||
|
["<leader>ns"] = {callback = M.show_saved_queries, desc = "Saved queries"},
|
||
|
["<leader>ni"] = {callback = M.index_workspace, desc = "Index workspace"},
|
||
|
["<leader>ns"] = {callback = M.show_index_status, desc = "Index status"},
|
||
|
["<leader>nc"] = {callback = M.cleanup_database, desc = "Cleanup database"},
|
||
|
["<leader>nv"] = {callback = M.switch_view_type, desc = "Switch view type"},
|
||
|
["<leader>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
|