diff --git a/lua/plugins/config/image.lua b/lua/plugins/config/image.lua new file mode 100644 index 0000000..807a5e5 --- /dev/null +++ b/lua/plugins/config/image.lua @@ -0,0 +1,47 @@ +local M = {} + +function M.setup() + local image = require 'image' + image:setup { + backend = 'kitty', + processor = 'magick_cli', + integrations = { + markdown = { + enabled = true, + clear_in_insert_mode = false, + download_remote_images = true, + only_render_image_at_cursor = false, + only_render_image_at_cursor_mode = 'popup', + floating_windows = false, + filetypes = { 'markdown', 'vimwiki' }, + }, + neorg = { + enabled = true, + filetypes = { 'norg' }, + }, + typst = { + enabled = true, + filetypes = { 'typst' }, + }, + html = { + enabled = false, + }, + css = { + enabled = false, + }, + }, + max_width = 100, + max_height = 12, + max_width_window_percentage = math.huge, + max_height_window_percentage = math.huge, + scale_factor = 1.0, + window_overlap_clear_enabled = true, + -- window_overlap_clear_ft_ignore = { 'cmp_menu', 'cmp_docs', 'snacks_notif', 'scrollview', 'scrollview_sign' }, + window_overlap_clear_ft_ignore = { 'cmp_menu', 'cmp_docs', '' }, + editor_only_render_when_focused = false, + tmux_show_only_in_active_window = false, + hijack_file_patterns = { '*.png', '*.jpg', '*.jpeg', '*.gif', '*.webp', '*.avif' }, + } +end + +return M diff --git a/lua/plugins/config/jupytext.lua b/lua/plugins/config/jupytext.lua new file mode 100644 index 0000000..9687ece --- /dev/null +++ b/lua/plugins/config/jupytext.lua @@ -0,0 +1,145 @@ +-- Jupytext configuration +local M = {} + +function M.setup() + -- Basic settings + vim.g.jupytext_fmt = 'py:percent' + vim.g.jupytext_command = 'jupytext' + + -- Create user commands + local function create_commands() + vim.api.nvim_create_user_command('JupytextSync', function() + vim.cmd('!jupytext --sync %') + vim.notify("Notebook synced", vim.log.levels.INFO) + end, { desc = "Sync current file with its notebook pair" }) + + vim.api.nvim_create_user_command('JupytextPair', function() + vim.cmd('!jupytext --set-formats ipynb,py:percent %') + vim.notify("Notebook paired with .py file", vim.log.levels.INFO) + end, { desc = "Pair notebook with Python file" }) + + vim.api.nvim_create_user_command('JupytextConvert', function(opts) + local format = opts.args ~= "" and opts.args or "py:percent" + vim.cmd('!jupytext --to ' .. format .. ' %') + vim.notify("Converted to " .. format, vim.log.levels.INFO) + end, { + nargs = '?', + desc = "Convert notebook to format (default: py:percent)", + complete = function() + return { 'py:percent', 'md', 'py:light', 'py:sphinx', 'jl:percent', 'r:percent' } + end + }) + end + + -- Setup auto-sync for paired files + local function setup_autosync() + local augroup = vim.api.nvim_create_augroup("JupytextSync", { clear = true }) + + vim.api.nvim_create_autocmd("BufWritePost", { + group = augroup, + pattern = { "*.py", "*.ipynb", "*.md", "*.jl", "*.r" }, + callback = function() + local file = vim.fn.expand('%:p') + local bufnr = vim.api.nvim_get_current_buf() + + -- Check if file has jupytext pairing metadata + local first_lines = vim.api.nvim_buf_get_lines(bufnr, 0, 15, false) + local has_pairing = false + local saw_jupytext = false + + for _, line in ipairs(first_lines) do + if line:match("jupytext") then + saw_jupytext = true + end + if saw_jupytext and line:match("formats:") then + has_pairing = true + break + end + end + + if has_pairing then + -- Run sync in background + vim.fn.jobstart({'jupytext', '--sync', file}, { + on_exit = function(_, exit_code) + if exit_code == 0 then + vim.schedule(function() + vim.notify("Jupytext: Synced with notebook", vim.log.levels.INFO) + end) + end + end + }) + end + end, + desc = "Auto-sync Jupytext paired files" + }) + end + + -- Setup keymaps + local function setup_keymaps() + -- Only set these in relevant filetypes + vim.api.nvim_create_autocmd("FileType", { + pattern = { "python", "markdown", "julia", "r" }, + callback = function() + local opts = { buffer = true, silent = true } + vim.keymap.set("n", "js", ":JupytextSync", + vim.tbl_extend("force", opts, { desc = "Sync with notebook" })) + vim.keymap.set("n", "jp", ":JupytextPair", + vim.tbl_extend("force", opts, { desc = "Pair with notebook" })) + vim.keymap.set("n", "jc", ":JupytextConvert ", + vim.tbl_extend("force", opts, { desc = "Convert to format" })) + end + }) + end + + -- Integration with Molten + local function setup_molten_integration() + -- When opening .ipynb files, offer to convert + vim.api.nvim_create_autocmd({"BufRead", "BufNewFile"}, { + pattern = "*.ipynb", + callback = function() + local file = vim.fn.expand('%:p') + local py_file = file:gsub('%.ipynb$', '.py') + + -- Check if .py version exists + if vim.fn.filereadable(py_file) == 1 then + -- Close the current buffer and open the .py version + vim.cmd('buffer #') -- Go to alternate buffer + vim.cmd('bdelete! #') -- Delete the ipynb buffer + vim.cmd('edit ' .. py_file) + vim.notify("Opened paired .py file instead of .ipynb", vim.log.levels.INFO) + else + -- Offer to convert + vim.schedule(function() + vim.ui.select( + { "Convert to .py", "Keep as JSON" }, + { prompt = "Convert notebook to Python?" }, + function(choice) + if choice == "Convert to .py" then + local result = vim.fn.system('jupytext --to py:percent "' .. file .. '"') + if vim.v.shell_error == 0 then + vim.cmd('edit! ' .. py_file) + vim.notify("Converted and opened as .py", vim.log.levels.INFO) + else + vim.notify("Failed to convert: " .. result, vim.log.levels.ERROR) + end + end + end + ) + end) + end + end, + desc = "Handle .ipynb files with Jupytext" + }) + end + + -- Initialize all components + create_commands() + setup_autosync() + setup_keymaps() + setup_molten_integration() +end + +-- Run setup +M.setup() + +return M diff --git a/lua/plugins/config/molten.lua b/lua/plugins/config/molten.lua index 7dc4eb5..db7fe0d 100644 --- a/lua/plugins/config/molten.lua +++ b/lua/plugins/config/molten.lua @@ -1,18 +1,94 @@ +-- Molten (Jupyter) configuration local M = {} +-- Keybindings +local function setup_keymaps() + local keymap = vim.keymap.set + + -- Basic operations + keymap('n', 'mi', ':MoltenInit', { desc = 'Molten Init', silent = true }) + keymap('n', 'ml', ':MoltenEvaluateLine', { desc = 'Evaluate Line', silent = true }) + keymap('n', 'mr', ':MoltenReevaluateCell', { desc = 'Re-evaluate Cell', silent = true }) + keymap('v', 'mv', ':MoltenEvaluateVisualgv', { desc = 'Evaluate Visual', silent = true }) + + -- Additional useful keymaps + keymap('n', 'md', ':MoltenDelete', { desc = 'Delete Cell', silent = true }) + keymap('n', 'mh', ':MoltenHideOutput', { desc = 'Hide Output', silent = true }) + keymap('n', 'mo', ':noautocmd MoltenEnterOutput', { desc = 'Enter Output', silent = true }) + + -- Better cell execution that always works + keymap('n', 'mc', function() + -- Always use the operator approach for consistency + -- This finds cell boundaries based on # %% markers + vim.cmd 'MoltenEvaluateOperator' + + -- Send 'ip' (inner paragraph) which respects the cell markers + local keys = vim.api.nvim_replace_termcodes('ip', true, false, true) + vim.api.nvim_feedkeys(keys, 'n', false) + end, { desc = 'Run Cell', silent = true }) + + -- Alternative: Run and move to next cell + keymap('n', 'mn', function() + vim.cmd 'MoltenEvaluateOperator' + local keys = vim.api.nvim_replace_termcodes('ip', true, false, true) + vim.api.nvim_feedkeys(keys, 'n', false) + -- Move to next cell + vim.fn.search('^# %%', 'W') + end, { desc = 'Run Cell and Next', silent = true }) + + -- Cell navigation + keymap('n', ']c', ':MoltenNext', { desc = 'Next Cell', silent = true }) + keymap('n', '[c', ':MoltenPrev', { desc = 'Previous Cell', silent = true }) +end + +-- Setup autocommands +local function setup_autocmds() + local augroup = vim.api.nvim_create_augroup('MoltenConfig', { clear = true }) + + -- Auto-initialize for Python files + vim.api.nvim_create_autocmd('FileType', { + group = augroup, + pattern = { 'python' }, + callback = function() + vim.keymap.set('n', 'ms', function() + vim.cmd 'MoltenInit python3' + vim.notify('Molten kernel started', vim.log.levels.INFO) + end, { buffer = true, desc = 'Start Python Kernel' }) + + -- Auto-detect notebook files with cell markers + vim.schedule(function() + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + for _, line in ipairs(lines) do + if line:match '^# %%' then + vim.notify('Notebook detected. Use ms to start kernel', vim.log.levels.INFO) + break + end + end + end) + end, + }) + + -- Better notebook file handling + vim.api.nvim_create_autocmd('BufRead', { + group = augroup, + pattern = { '*.ipynb' }, + callback = function() + -- Consider using jupytext for better .ipynb handling + vim.notify('Notebook file detected. Use :MoltenInit to start kernel', vim.log.levels.INFO) + end, + }) +end + +-- Main setup function function M.setup() - -- Keybindings - local map = vim.keymap.set - - map('n', 'Mi', ':MoltenInit', { desc = 'Molten: Initialize' }) - map('n', 'Me', ':MoltenEvaluateOperator', { desc = 'Molten: Evaluate operator' }) - map('v', 'Me', ':MoltenEvaluateVisualgv', { desc = 'Molten: Evaluate visual' }) - map('n', 'Ml', ':MoltenEvaluateLine', { desc = 'Molten: Evaluate line' }) - map('n', 'Mr', ':MoltenReevaluateCell', { desc = 'Molten: Re-evaluate cell' }) - map('n', 'Md', ':MoltenDelete', { desc = 'Molten: Delete cell' }) - map('n', 'Mo', ':MoltenShowOutput', { desc = 'Molten: Show output' }) - map('n', 'Mh', ':MoltenHideOutput', { desc = 'Molten: Hide output' }) - map('n', 'Mx', ':MoltenInterrupt', { desc = 'Molten: Interrupt kernel' }) + -- Tell Molten about cell delimiters + vim.g.molten_cell_delimiter = '^# %%' + + setup_keymaps() + setup_autocmds() end +-- Initialize +M.setup() + return M diff --git a/lua/plugins/spec/image.lua b/lua/plugins/spec/image.lua new file mode 100644 index 0000000..be5bf33 --- /dev/null +++ b/lua/plugins/spec/image.lua @@ -0,0 +1,8 @@ +-- Render images in neovim +return { + '3rd/image.nvim', + build = false, + config = function() + require('plugins.config.image').setup() + end, +} diff --git a/lua/plugins/spec/jupytext.lua b/lua/plugins/spec/jupytext.lua new file mode 100644 index 0000000..52d4cf3 --- /dev/null +++ b/lua/plugins/spec/jupytext.lua @@ -0,0 +1,12 @@ +-- Jupytext - Jupyter notebook as plain text +return { + 'goerz/jupytext.vim', + ft = { 'ipynb', 'python', 'markdown', 'julia', 'r' }, + build = function() + -- Ensure jupytext is installed + vim.fn.system('pip install jupytext') + end, + config = function() + require('plugins.config.jupytext') + end, +} diff --git a/lua/plugins/spec/molten.lua b/lua/plugins/spec/molten.lua new file mode 100644 index 0000000..0abec4b --- /dev/null +++ b/lua/plugins/spec/molten.lua @@ -0,0 +1,19 @@ +-- Molten - Jupyter notebook support in Neovim +return { + 'benlubas/molten-nvim', + version = '^1.0.0', + dependencies = { '3rd/image.nvim' }, + build = ':UpdateRemotePlugins', + init = function() + -- Basic settings + vim.g.molten_image_provider = 'image.nvim' + vim.g.molten_output_win_max_height = 20 + vim.g.molten_auto_open_output = true + vim.g.molten_wrap_output = true + vim.g.molten_virt_text_output = true + end, + config = function() + -- Delegate to config file for complex setup + require('plugins.config.molten') + end, +}