diff --git a/.gitignore b/.gitignore index 005b535b606..8a192cab54d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,3 @@ test.sh nvim spell/ -lazy-lock.json diff --git a/init.lua b/init.lua index 22ee9e4878d..79614e26637 100644 --- a/init.lua +++ b/init.lua @@ -189,6 +189,355 @@ vim.keymap.set('n', '', '', { desc = 'Move focus to the right win vim.keymap.set('n', '', '', { desc = 'Move focus to the lower window' }) vim.keymap.set('n', '', '', { desc = 'Move focus to the upper window' }) +vim.api.nvim_set_keymap('c', '%%', "=expand('%:h').'/'", { noremap = true, silent = true }) + +-- Obsidian Integration (Optimized) +local M = {} + +--[[ Module Design: +1. Encapsulated configuration +2. Separated concerns with dedicated modules +3. Strict local scoping +4. Error handling consistency +5. Documentation-ready structure +6. Reduced code duplication +]] + +-- Configuration Module -- +local Config = { + path = vim.fn.expand '~/.config/nvim/obsidian_vaults.json', + vaults = {}, + default_vault_name = 'default', + temp_dir_suffix = '_temp_preview', +} + +-- Utility Module -- +local Utils = {} + +function Utils.is_executable(cmd) + return vim.fn.executable(cmd) == 1 +end + +function Utils.ensure_dir(path) + return vim.fn.mkdir(path, 'p') == 1 +end + +function Utils.safe_path(path) + return (path:gsub([[\]], [[/]]):gsub('/$', '') .. '/') +end + +function Utils.notify(msg, level) + vim.notify(msg, level or vim.log.levels.INFO) +end + +function Utils.url_encode(str) + if Utils.is_executable 'python3' then + local handle = io.popen(string.format('python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))" %q 2>/dev/null', str)) + if handle then + local result = handle:read '*a' + handle:close() + return result:gsub('\n', '') + end + end + + return str:gsub('[^%w%-%.%_%~]', function(c) + return string.format('%%%02X', string.byte(c)) + end) +end + +-- If vim.pesc is not defined, define it to escape Lua patterns. +if not vim.pesc then + vim.pesc = function(str) + return str:gsub('([^%w])', '%%%1') + end +end + +-- Vault Manager Module -- +local VaultManager = {} + +function VaultManager.load() + local file = io.open(Config.path, 'r') + if not file then + return VaultManager.create_default() + end + + local success, parsed = pcall(vim.json.decode, file:read '*a') + file:close() + + if success and type(parsed) == 'table' then + Config.vaults = parsed + return true + end + + return VaultManager.create_default() +end + +function VaultManager.create_default() + local default_path = Utils.safe_path(vim.fn.expand '~/obsidian-vault/') + if Utils.ensure_dir(default_path) then + Config.vaults = { { + name = Config.default_vault_name, + path = default_path, + } } + return VaultManager.save() + end + return false +end + +function VaultManager.save() + local file = io.open(Config.path, 'w') + if not file then + Utils.notify('Failed to save vault config', vim.log.levels.ERROR) + return false + end + + local success, encoded = pcall(vim.json.encode, Config.vaults) + if not success then + file:close() + Utils.notify('Config serialization failed', vim.log.levels.ERROR) + return false + end + + file:write(encoded) + file:close() + return true +end + +function VaultManager.find_containing(filepath) + local safe_path = Utils.safe_path(filepath) + for _, vault in ipairs(Config.vaults) do + local vault_path = Utils.safe_path(vault.path) + if safe_path:find('^' .. vim.pesc(vault_path)) then + return vault, safe_path:gsub('^' .. vim.pesc(vault_path), '') + end + end + return nil, nil +end + +-- Obsidian Core -- +local Obsidian = {} + +function Obsidian.open() + local filepath = vim.fn.expand '%:p' + if filepath == '' then + return Utils.notify('No file to open', vim.log.levels.WARN) + end + if not filepath:match '%.md$' then + return Utils.notify('Not a markdown file', vim.log.levels.WARN) + end + + local vault, rel_path = VaultManager.find_containing(filepath) + if vault then + Obsidian.open_vault_file(vault, rel_path) + else + Obsidian.open_external_file(filepath) + end +end + +function Obsidian.open_vault_file(vault, rel_path) + local encoded = Utils.url_encode(rel_path) + local uri = ('obsidian://open?vault=%s&file=%s'):format(vault.name, encoded) + Utils.notify(("Opening in '%s': %s"):format(vault.name, rel_path)) + Obsidian.execute({ 'xdg-open', uri }, true) +end + +function Obsidian.open_external_file(filepath) + local default_vault = Config.vaults[1] + if not default_vault then + return Utils.notify('No default vault', vim.log.levels.ERROR) + end + + local temp_dir = Utils.safe_path(default_vault.path .. Config.temp_dir_suffix) + if not Utils.ensure_dir(temp_dir) then + return + end + + local basename = vim.fn.fnamemodify(filepath, ':t') + local symlink = temp_dir .. basename + + os.remove(symlink) + if not vim.loop.fs_symlink(filepath, symlink) then + return Utils.notify('Symlink failed', vim.log.levels.ERROR) + end + + -- Add these lines to refresh Obsidian on save + vim.api.nvim_create_autocmd({ 'BufWritePost' }, { + buffer = vim.api.nvim_get_current_buf(), + callback = function() + -- Update symlink metadata to force Obsidian refresh + os.execute(string.format('touch -h %q', symlink)) -- -h flag affects symlink itself + end, + }) + + vim.api.nvim_create_autocmd({ 'BufDelete', 'BufWipeout' }, { + buffer = vim.api.nvim_get_current_buf(), + callback = function() + os.remove(symlink) + end, + }) + + local encoded = Utils.url_encode(Config.temp_dir_suffix .. '/' .. basename) + local uri = ('obsidian://open?vault=%s&file=%s'):format(default_vault.name, encoded) + Obsidian.execute({ 'xdg-open', uri }, true) +end + +function Obsidian.close(detach) + if not Utils.is_executable 'pkill' then + return + end + + local opts = { + detach = detach or false, + on_exit = not detach and function(_, code) + if code == 0 then + Utils.notify 'Obsidian closed' + end + end, + } + + Obsidian.execute({ 'pkill', '-9', '-f', 'obsidian' }, opts.detach, opts.on_exit) +end + +function Obsidian.execute(command, detach, callback) + vim.fn.jobstart(command, { + detach = detach, + on_exit = callback or function(_, code) + if code ~= 0 then + Utils.notify('Command failed', vim.log.levels.ERROR) + end + end, + }) +end + +-- Public API -- +function M.add_vault(name, path) + path = Utils.safe_path(vim.fn.expand(path)) + + for _, vault in ipairs(Config.vaults) do + if vault.name == name then + return Utils.notify('Vault name exists', vim.log.levels.WARN) + end + if Utils.safe_path(vault.path) == path then + return Utils.notify('Vault path exists', vim.log.levels.WARN) + end + end + + if not Utils.ensure_dir(path) then + return Utils.notify('Directory creation failed', vim.log.levels.ERROR) + end + + table.insert(Config.vaults, { name = name, path = path }) + return VaultManager.save() and Utils.notify(('Added vault: %s'):format(name)) +end + +function M.remove_vault(name) + for i, vault in ipairs(Config.vaults) do + if vault.name == name then + table.remove(Config.vaults, i) + return VaultManager.save() and Utils.notify(('Removed vault: %s'):format(name)) + end + end + Utils.notify('Vault not found', vim.log.levels.WARN) +end + +function M.list_vaults() + local lines = { 'Configured Vaults:' } + for _, vault in ipairs(Config.vaults) do + table.insert(lines, ('- %s: %s'):format(vault.name, vault.path)) + end + Utils.notify(table.concat(lines, '\n')) +end + +-- Initialization -- +VaultManager.load() + +-- Command Setup -- +vim.api.nvim_create_user_command('OpenInObsidian', Obsidian.open, {}) +vim.api.nvim_create_user_command('CloseObsidian', function() + Obsidian.close() +end, {}) +vim.api.nvim_create_user_command('ListObsidianVaults', M.list_vaults, {}) +vim.api.nvim_create_user_command('AddObsidianVault', function(opts) + local args = vim.split(opts.args, '%s+', { trimempty = true }) + if #args == 2 then + M.add_vault(args[1], args[2]) + end +end, { nargs = '+' }) + +vim.api.nvim_create_user_command('RemoveObsidianVault', function(opts) + M.remove_vault(opts.args) +end, { nargs = 1 }) + +-- Keymaps -- +local wk_ok, wk = pcall(require, 'which-key') +local keymaps = { + ['o'] = { + name = 'Obsidian', + o = { Obsidian.open, 'Open' }, + c = { + function() + Obsidian.close() + end, + 'Close', + }, + l = { M.list_vaults, 'List' }, + a = { + function() + vim.ui.input({ prompt = 'Vault name: ' }, function(name) + if name then + vim.ui.input({ prompt = 'Path: ' }, function(path) + if path then + M.add_vault(name, path) + end + end) + end + end) + end, + 'Add Vault', + }, + r = { + function() + vim.ui.input({ prompt = 'Vault to remove: ' }, function(name) + if name then + M.remove_vault(name) + end + end) + end, + 'Remove Vault', + }, + }, +} + +if wk_ok then + wk.register(keymaps) +else + for lhs, rhs in pairs(keymaps['o']) do + if type(rhs) == 'table' then + vim.keymap.set('n', 'o' .. lhs, rhs[1], { desc = rhs[2] }) + end + end +end + +-- Autocommands -- +vim.api.nvim_create_augroup('ObsidianIntegration', { clear = true }) + +vim.api.nvim_create_autocmd({ 'FocusGained', 'BufEnter' }, { + group = 'ObsidianIntegration', + callback = vim.schedule_wrap(function() + vim.cmd 'checktime' + end), +}) + +vim.api.nvim_create_autocmd('VimLeavePre', { + group = 'ObsidianIntegration', + callback = function() + for _, vault in ipairs(Config.vaults) do + os.execute(('rm -rf %q'):format(vault.path .. Config.temp_dir_suffix)) + end + -- Obsidian.close(true) + end, +}) + -- [[ Basic Autocommands ]] -- See `:help lua-guide-autocommands` @@ -203,6 +552,12 @@ vim.api.nvim_create_autocmd('TextYankPost', { end, }) +vim.api.nvim_exec( + [[ + autocmd BufNewFile ~/cavelazquez8-wiki/diary/*.md :silent 0r !~/.vim/autoload/vimwiki/generate-vimwiki-diary-template '%' + ]], + false +) -- [[ Install `lazy.nvim` plugin manager ]] -- See `:help lazy.nvim.txt` or https://github.com/folke/lazy.nvim for more info local lazypath = vim.fn.stdpath 'data' .. '/lazy/lazy.nvim' @@ -718,6 +1073,9 @@ require('lazy').setup({ end, }, } + require('lspconfig').clangd.setup { + capabilities = capabilities, + } end, }, @@ -991,8 +1349,7 @@ require('lazy').setup({ -- This is the easiest way to modularize your config. -- -- Uncomment the following line and add your plugins to `lua/custom/plugins/*.lua` to get going. - -- { import = 'custom.plugins' }, - -- + { import = 'custom.plugins' }, -- For additional information with loading, sourcing and examples see `:help lazy.nvim-🔌-plugin-spec` -- Or use telescope! -- In normal mode type `sh` then write `lazy.nvim-plugin` diff --git a/lazy-lock.json b/lazy-lock.json new file mode 100644 index 00000000000..e60acdb43f2 --- /dev/null +++ b/lazy-lock.json @@ -0,0 +1,28 @@ +{ + "LuaSnip": { "branch": "master", "commit": "3732756842a2f7e0e76a7b0487e9692072857277" }, + "cmp-nvim-lsp": { "branch": "main", "commit": "bd5a7d6db125d4654b50eeae9f5217f24bb22fd3" }, + "cmp-nvim-lsp-signature-help": { "branch": "main", "commit": "031e6ba70b0ad5eee49fd2120ff7a2e325b17fa7" }, + "cmp-path": { "branch": "main", "commit": "c642487086dbd9a93160e1679a1327be111cbc25" }, + "cmp_luasnip": { "branch": "master", "commit": "98d9cb5c2c38532bd9bdb481067b20fea8f32e90" }, + "conform.nvim": { "branch": "master", "commit": "cde4da5c1083d3527776fee69536107d98dae6c9" }, + "fidget.nvim": { "branch": "main", "commit": "e32b672d8fd343f9d6a76944fedb8c61d7d8111a" }, + "gitsigns.nvim": { "branch": "main", "commit": "20ad4419564d6e22b189f6738116b38871082332" }, + "lazy.nvim": { "branch": "main", "commit": "e6a8824858757ca9cd4f5ae1a72d845fa5c46a39" }, + "lazydev.nvim": { "branch": "main", "commit": "371cd7434cbf95606f1969c2c744da31b77fcfa6" }, + "mason-lspconfig.nvim": { "branch": "main", "commit": "d7b5feb6e769e995f7fcf44d92f49f811c51d10c" }, + "mason-tool-installer.nvim": { "branch": "main", "commit": "517ef5994ef9d6b738322664d5fdd948f0fdeb46" }, + "mason.nvim": { "branch": "main", "commit": "ad7146aa61dcaeb54fa900144d768f040090bff0" }, + "mini.nvim": { "branch": "main", "commit": "7d6fcfd879fe520aba187fe2bd25078fa2367d03" }, + "nvim-cmp": { "branch": "main", "commit": "106c4bcc053a5da783bf4a9d907b6f22485c2ea0" }, + "nvim-lspconfig": { "branch": "master", "commit": "2010fc6ec03e2da552b4886fceb2f7bc0fc2e9c0" }, + "nvim-treesitter": { "branch": "master", "commit": "42fc28ba918343ebfd5565147a42a26580579482" }, + "plenary.nvim": { "branch": "master", "commit": "b9fd5226c2f76c951fc8ed5923d85e4de065e509" }, + "telescope-fzf-native.nvim": { "branch": "main", "commit": "1f08ed60cafc8f6168b72b80be2b2ea149813e55" }, + "telescope-ui-select.nvim": { "branch": "master", "commit": "6e51d7da30bd139a6950adf2a47fda6df9fa06d2" }, + "telescope.nvim": { "branch": "0.1.x", "commit": "a0bbec21143c7bc5f8bb02e0005fa0b982edc026" }, + "todo-comments.nvim": { "branch": "main", "commit": "411503d3bedeff88484de572f2509c248e499b38" }, + "tokyonight.nvim": { "branch": "main", "commit": "b13cfc1286d2aa8bda6ce137b79e857d5a3d5739" }, + "vim-sleuth": { "branch": "master", "commit": "be69bff86754b1aa5adcbb527d7fcd1635a84080" }, + "vimwiki": { "branch": "dev", "commit": "72792615e739d0eb54a9c8f7e0a46a6e2407c9e8" }, + "which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" } +} diff --git a/lua/custom/plugins/wiki.lua b/lua/custom/plugins/wiki.lua new file mode 100644 index 00000000000..0acf1420b22 --- /dev/null +++ b/lua/custom/plugins/wiki.lua @@ -0,0 +1,47 @@ +return { + 'vimwiki/vimwiki', + keys = { + 'ww', + -- "wt", + -- { "wz", "Vimwiki2HTML", desc = "Vimwiki2HTML" }, + -- { "wx", "VimwikiAll2HTML", desc = "VimwikiAll2HTML" }, + }, + cmd = 'VimwikiIndex', -- Add this command to load Vimwiki + init = function() + vim.g.vimwiki_list = { + { + path = '~/cavelazquez8-wiki/', + syntax = 'markdown', + ext = '.md', + -- Custom Markdown to HTML converter + -- custom_wiki2html = "~/.vim/autoload/vimwiki/convert.py", + -- Auto update diary index + index = 'mocs/main-moc', + -- auto_diary_index = 1, + links_space_char = '-', -- Replace spaces with hyphens in links + }, + { + path = '~/personal-wiki/', + syntax = 'markdown', + ext = '.md', + index = 'mocs/main-moc', + -- Custom Markdown to HTML converter + -- custom_wiki2html = "~/.vim/autoload/vimwiki/convert.py", + -- Auto update diary index + -- auto_diary_index = 1, + links_space_char = '-', -- Replace spaces with hyphens in links + }, + } + -- Register markdown files + -- vim.g.vimwiki_ext2syntax = { + -- ['.md'] = 'markdown', + -- ['.markdown'] = 'markdown', + -- ['.mdown'] = 'markdown', + --} + -- prevent md files from being coverted to vimwiki files + vim.g.vimwiki_global_ext = 0 + -- Add .md extension to link + vim.g.vimwiki_markdown_link_ext = 1 + -- vim.g.vimwiki_filetypes = { 'markdown' } + end, +}