如何让neovim 集成 jupyter

前言

虽然可以为jupyter安装 jupyter-lsp 插件,实现 pythonlsp 相关功能,但是编码体验仍然不如我平时使用的 neovim,在查阅github后,发现已有neovim插件实现了这方面的功能,不过配置起来略微有些繁琐,顾决定将操作步骤记录下来,以便于后者学习参考。

前置环境

我的系统和相关工具链版本

  • OS: macOS 15.3 arm64
  • Kernel: 24.3.0
  • Packages: 171 (brew)
  • Shell: zsh 5.9
  • NVIM v0.10.4
  • Pyenv 2.4.23 + Python 3.12.8

博主 neovim 配置模板用的基于 lazy.nvimLazyVim ,所以一会儿也会将 molten-nvim插件中的教程转换为LazyVim插件的形式,其他配置文件模板请参考对应模板的语法。不过现阶段几乎所有的 neovim 模板都基于 lazy.vim,所以插件其实是通用的。

以下是一会儿需要用到的neovim 插件。

安装 molten 所需 python 依赖

此处参考文档1 和文档 2

其中,molten 会用到 python,博主使用 zinit 来管理 zsh 的相关插件,pyenv 是通过 zinit 来安装的,以下是我的配置参考

## pyenv
export PYENV_ROOT=$ZPFX/pyenv;
export WORKON_HOME=$PYENV_ROOT/versions
export PROJECT_HOME=$PYENV_ROOT/Devel
zinit ice wait lucid as"program" depth"1" cloneopts"" pick"$ZPFX/pyenv/libexec/pyenv" id-as'pyenv' run-atpull \
  atclone"echo pyenv installing at clone; export PYENV_ROOT=$ZPFX/pyenv; echo PYENV_ROOT: $ZPFX/pyenv; curl -fsSL https://pyenv.run | bash; $ZPFX/pyenv/libexec/pyenv init - | tee $ZPFX/pyenv/init.zsh" \
  atpull"echo pyenv updating at pull; %atclone" \
  atinit"source $ZPFX/pyenv/init.zsh;"
zinit light zdharma-continuum/null

molten的文档中可以注意到,他用的是 virtualenvs来管理插件所需要的 python 依赖包,这里我用的是 pyenv,所以使用的是 pyenv 自带的 pyenv-virtualenv插件来创建虚拟环境,原理大同小异,也可以自定义使用其他虚拟包环境管理器。

使用以下命令创建并激活一个名为 neovim 的虚拟环境,随后使用 pip 安装 molten 所需 python 依赖。

pyenv virtualenv neovim
pyenv activate neovim
pip install pynvim jupyter_client cairosvg plotly kaleido pnglatex pyperclip nbformat

接着按照文档,修改 neovim 的配置,使其能找到当前虚拟环境中的 python

这里要注意,请先使用以下命令查看自己的 neovim 虚拟环境安装目录中,python 的路径

echo $VIRTUAL_ENV/bin/python3

博主的输出是如下内容

/Users/styunlen/.zinit/polaris/pyenv/versions/neovim/bin/python3

所以接下来我就在 neovim 的配置里写以下内容,如果你也像我一样在 zshrc 中声明了WORKON_HOME变量为$PYENV_ROOT/versions,则可以使用下面注释掉的那个写法

vim.g.python3_host_prog = vim.fn.expand("~/.zinit/polaris/pyenv/versions/neovim/bin/python3")
-- vim.g.python3_host_prog = os.getenv("WORKON_HOME") .. "/neovim/bin/python3"

这些都操作完之后,我们进入工作目录,比如我的工作目录为~/WorkDir/Comp,然后重新使用 pyenv(可以自定义)创建并激活一个包含数据分析、人工智能、数据可视化等常见python模块的虚拟环境common

然后安装 ipykernel包,并添加一个名为 neovimipykernel核心,用于 jupyter 后期使用。

以下是相关命令:

pyenv virtualenv common
pyenv activate common
pip install ipykernel
python -m ipykernel install --user --name neovim

配置 neovim

此处参考文档 3

主要用于添加 neovimipynb 的直接编辑支持

由于我使用 LazyVim,所以单独编写一个 lua 文件来配置与 molten 相关插件。如不想看详细步骤,可以直接跳转到第二页复制完整配置

使用以下命令配置插件

nvim ~/.config/nvim/lua/plugins/molten.lua

然后将最开始提到过的插件都加入其中,搭建一个框架

return {
  {
    "benlubas/molten-nvim",
    version = "^1.0.0", -- use version <2.0.0 to avoid breaking changes
    dependencies = { "3rd/image.nvim" },
    build = ":UpdateRemotePlugins",
    init = function()
    end,
  },
  { -- requires plugins in lua/plugins/treesitter.lua and lua/plugins/lsp.lua
    -- for complete functionality (language features)
    "quarto-dev/quarto-nvim",
    ft = { "quarto", "markdown" },
    dev = false,
    config = function()
    end,
    dependencies = {
      -- for language features in code cells
      -- configured in lua/plugins/lsp.lua and
      -- added as a nvim-cmp source in lua/plugins/completion.lua
      "jmbuhr/otter.nvim",
      "nvim-treesitter/nvim-treesitter",
    },
  },
  {
    "GCBallesteros/jupytext.nvim",
    opts = {
      style = "markdown",
      output_extension = "md",
      force_ft = "markdown",
    },
  },
  { -- preview equations
    "jbyuki/nabla.nvim",
    keys = {
      { "<leader>qm", ':lua require"nabla".toggle_virt()<cr>', desc = "toggle [m]ath equations" },
    },
  },
  {
    "nvim-lualine/lualine.nvim",
    dependencies = {
      "benlubas/molten-nvim",
    },
    event = "VeryLazy",
    opts = function(_, opts)
      table.insert(opts.sections.lualine_y, {
        function()
          return require("molten.status").kernels() .. " (ipykernel)"
        end,
      })
    end,
  },
  {
    -- see the image.nvim readme for more information about configuring this plugin
    "3rd/image.nvim",
    opts = {
      backend = "kitty", -- whatever backend you would like to use
      processor = "magick_cli", -- or "magick_cli"
      max_width = 100,
      max_height = 12,
      max_height_window_percentage = math.huge,
      max_width_window_percentage = math.huge,
      window_overlap_clear_enabled = true, -- toggles images when windows are overlapped
      window_overlap_clear_ft_ignore = { "cmp_menu", "cmp_docs", "" },
    },
  },
}

接下来的步骤主要是分布完成 neovimjupyter 以下四个功能的集成

  • 代码运行
  • 结果预览
  • markdownPython LSP 的相关功能 (自动补全, 定义引用跳转, 快速重命名, 代码格式化等)
  • 文件格式转换

上述框架中已经完成了插件集成和联动,比如 lualine 显示当前激活的 ipykernel,但还没有添加快捷键映射、文件自动处理等功能,我们接下来根据上述框架,从上到下一一完善这个配置。

benlubas/molten-nvim

首先完善benlubas/molten-nvim插件的 init 方法,主要实现了以下几个功能

  • neovim 能找到我们为 molten 插件单独创建的虚拟环境,上面有个地方就是添加此处配置,只不过我把他集成到了此处
  • 开启一些官方推荐的插件选项
  • 官方推荐的快捷键绑定
  • 打开 ipynb 文件时,自动初始化 moltenquarto插件,并导入已有的运行结果数据;关闭时,自动导出运行结果数据
  • 添加新建 jupyter notebook的指令

代码如下

init = function()
  local function getenv(var)
    local v = os.getenv(var)
    if v == nil then
      return ""
    else
      return v
    end
  end
  -- vim.g.python3_host_prog=vim.fn.expand("")
  vim.g.python3_host_prog = getenv("WORKON_HOME") .. "/neovim/bin/python3"

  -- I find auto open annoying, keep in mind setting this option will require setting
  -- a keybind for `:noautocmd MoltenEnterOutput` to open the output again
  vim.g.molten_auto_open_output = false

  -- this guide will be using image.nvim
  -- Don't forget to setup and install the plugin if you want to view image outputs
  vim.g.molten_image_provider = "image.nvim"

  -- optional, I like wrapping. works for virt text and the output window
  vim.g.molten_wrap_output = true

  -- Output as virtual text. Allows outputs to always be shown, works with images, but can
  -- be buggy with longer images
  vim.g.molten_virt_text_output = true

  -- this will make it so the output shows up below the \`\`\` cell delimiter
  vim.g.molten_virt_lines_off_by_1 = true
  -- these are examples, not defaults. Please see the readme
  vim.g.molten_image_provider = "image.nvim"
  vim.g.molten_output_win_max_height = 20
  vim.keymap.set("n", "<localleader>mi", ":MoltenInit<CR>", { silent = true, desc = "Initialize the plugin" })
  vim.keymap.set(
    "n",
    "<localleader>e",
    ":MoltenEvaluateOperator<CR>",
    { silent = true, desc = "run operator selection" }
  )
  vim.keymap.set("n", "<localleader>rl", ":MoltenEvaluateLine<CR>", { silent = true, desc = "evaluate line" })
  vim.keymap.set("n", "<localleader>rr", ":MoltenReevaluateCell<CR>", { silent = true, desc = "re-evaluate cell" })
  vim.keymap.set(
    "v",
    "<localleader>rv",
    ":<C-u>MoltenEvaluateVisual<CR>gv",
    { silent = true, desc = "evaluate visual selection" }
  )
  vim.keymap.set("n", "<localleader>rd", ":MoltenDelete<CR>", { silent = true, desc = "molten delete cell" })
  vim.keymap.set("n", "<localleader>oh", ":MoltenHideOutput<CR>", { silent = true, desc = "hide output" })
  vim.keymap.set(
    "n",
    "<localleader>os",
    ":noautocmd MoltenEnterOutput<CR>",
    { silent = true, desc = "show/enter output" }
  )
  -- if you work with html outputs:
  vim.keymap.set(
    "n",
    "<localleader>mx",
    ":MoltenOpenInBrowser<CR>",
    { desc = "open output in browser", silent = true }
  )
  local imb = function(e) -- init molten buffer
    vim.schedule(function()
      vim.cmd("MoltenInit")
      vim.cmd("MoltenImportOutput")
      vim.cmd("QuartoActivate")
    end)
  end
  -- automatically import output chunks from a jupyter notebook
  vim.api.nvim_create_autocmd("BufAdd", {
    pattern = { "*.ipynb" },
    callback = imb,
  })

  -- we have to do this as well so that we catch files opened like nvim ./hi.ipynb
  vim.api.nvim_create_autocmd("BufEnter", {
    pattern = { "*.ipynb" },
    callback = function(e)
      if vim.api.nvim_get_vvar("vim_did_enter") ~= 1 then
        imb(e)
      end
    end,
  })
  -- automatically export output chunks to a jupyter notebook on write
  vim.api.nvim_create_autocmd("BufWritePost", {
    pattern = { "*.ipynb" },
    callback = function()
      if require("molten.status").initialized() == "Molten" then
        vim.cmd("MoltenExportOutput!")
      end
    end,
  })

  -- Provide a command to create a blank new Python notebook
  -- note: the metadata is needed for Jupytext to understand how to parse the notebook.
  -- if you use another language than Python, you should change it in the template.
  local default_notebook = [[
      {
      "cells": [
      {
        "cell_type": "markdown",
        "metadata": {},
        "source": [
          ""
        ]
      }
      ],
      "metadata": {
      "kernelspec": {
        "display_name": "Python 3",
        "language": "python",
        "name": "python3"
      },
      "language_info": {
        "codemirror_mode": {
          "name": "ipython"
        },
        "file_extension": ".py",
        "mimetype": "text/x-python",
        "name": "python",
        "nbconvert_exporter": "python",
        "pygments_lexer": "ipython3"
      }
      },
      "nbformat": 4,
      "nbformat_minor": 5
    }
  ]]

  local function new_notebook(filename)
    local path = filename .. ".ipynb"
    local file = io.open(path, "w")
    if file then
      file:write(default_notebook)
      file:close()
      vim.cmd("edit " .. path)
    else
      print("Error: Could not open new notebook file for writing.")
    end
  end

  vim.api.nvim_create_user_command("NewNotebook", function(opts)
    new_notebook(opts.args)
  end, {
    nargs = 1,
    complete = "file",
  })
end,

quarto-dev/quarto-nvim

文档中在做这一步之前,需要先为 nvim-treesitter 添加一个新的选择器,方便我们在编辑`ipynb时,快速上下跳转,可以输入以下命令快捷完成

cat >~/.config/nvim/after/queries/markdown/textobjects.scm <<EOF
;; extends

(fenced_code_block (code_fence_content) @code_cell.inner) @code_cell.outer
EOF

然后配置 quarto-dev/quarto-nvim插件的config函数,主要实现以下几个功能

  • code runner 设置为 molten,连接两个插件
  • 使用一些官方文档推荐的配置和快捷键
  • 扩展nvim-treesitter插件

代码如下

config = function()
  require("quarto").setup({
    lspFeatures = {
      -- NOTE: put whatever languages you want here:
      languages = { "r", "python", "rust" },
      chunks = "all",
      diagnostics = {
        enabled = true,
        triggers = { "BufWritePost" },
      },
      completion = {
        enabled = true,
      },
    },
    keymap = {
      -- NOTE: setup your own keymaps:
      hover = "H",
      definition = "gd",
      rename = "<leader>rn",
      references = "gr",
      format = "<leader>gf",
    },
    codeRunner = {
      enabled = true,
      default_method = "molten",
    },
  })
  local runner = require("quarto.runner")
  vim.keymap.set("n", "<localleader>rc", runner.run_cell, { desc = "run cell", silent = true })
  vim.keymap.set("n", "<localleader>ra", runner.run_above, { desc = "run cell and above", silent = true })
  vim.keymap.set("n", "<localleader>rA", runner.run_all, { desc = "run all cells", silent = true })
  vim.keymap.set("n", "<localleader>rl", runner.run_line, { desc = "run line", silent = true })
  vim.keymap.set("v", "<localleader>r", runner.run_range, { desc = "run visual range", silent = true })
  vim.keymap.set("n", "<localleader>RA", function()
    runner.run_all(true)
  end, { desc = "run all cells of all languages", silent = true })
  require("nvim-treesitter.configs").setup({
    -- ... other ts config
    textobjects = {
      move = {
        enable = true,
        set_jumps = false, -- you can change this if you want.
        goto_next_start = {
          --- ... other keymaps
          ["]b"] = { query = "@code_cell.inner", desc = "next code block" },
        },
        goto_previous_start = {
          --- ... other keymaps
          ["[b"] = { query = "@code_cell.inner", desc = "previous code block" },
        },
      },
      select = {
        enable = true,
        lookahead = true, -- you can change this if you want
        keymaps = {
          --- ... other keymaps
          ["ib"] = { query = "@code_cell.inner", desc = "in block" },
          ["ab"] = { query = "@code_cell.outer", desc = "around block" },
        },
      },
      swap = { -- Swap only works with code blocks that are under the same
        -- markdown header
        enable = true,
        swap_next = {
          --- ... other keymap
          ["<leader>sbl"] = "@code_cell.outer",
        },
        swap_previous = {
          --- ... other keymap
          ["<leader>sbh"] = "@code_cell.outer",
        },
      },
    },
  })
end,

总结

完成以上内容后,就基本完成了所有配置,我们进入工作目录随意打开一个jupyter notebook试试,OK,完成!

参考文档

  1. molten-nvim/docs/Not-So-Quick-Start-Guide.md at main · benlubas/molten-nvim · GitHub
  2. molten-nvim/docs/Virtual-Environments.md at main · benlubas/molten-nvim · GitHub
  3. molten-nvim/docs/Notebook-Setup.md at main · benlubas/molten-nvim · GitHub
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
Source: https://github.com/zhaoolee/ChineseBQB
Source: https://github.com/zhaoolee/ChineseBQB
Source: https://github.com/zhaoolee/ChineseBQB
颜文字
Emoji
小恐龙
花!
滑稽大佬
演奏
程序员专属
上一篇
下一篇