├── .gitignore ├── LICENSE ├── README.md ├── doc └── nvim-python-repl.txt ├── lua └── nvim-python-repl │ ├── config.lua │ ├── init.lua │ └── nvim-python-repl.lua └── plugin └── nvim-python-repl.vim /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | 43 | dev/init.lua 44 | test.py 45 | test.scala 46 | test.lua -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Geoffrey Grossman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-python-repl 2 | [![asciicast](https://asciinema.org/a/34uNXyyhsDJFDBFSAzzrf9B1b.svg)](https://asciinema.org/a/34uNXyyhsDJFDBFSAzzrf9B1b) 3 | 4 | A simple plugin that leverages treesitter to send expressions, statements, 5 | function definitions and class definitions to a REPL. 6 | 7 | The plugin now supports three different filetypes: python, scala and lua. It is 8 | suggested that you have [ipython](https://ipython.org/), 9 | [sbt](https://www.scala-sbt.org), and [ilua](https://github.com/guysv/ilua) 10 | installed in your path respectively. (Scala projects are expecting that the scala file 11 | is opened from the directory containing `build.sbt`). 12 | 13 | In addition to sending treesitter objects, there is also support for sending a 14 | selection from visual mode. 15 | 16 | ### Requirements 17 | #### tree-sitter parser 18 | You will need to install [the tree-sitter parser that corresponds to your desired programming language](https://github.com/tree-sitter). For example, for Python, you will need to execute `:TSInstall python` before `nvim-python-repl` will function properly. 19 | 20 | ### Usage 21 | 22 | Can be installed with any plugin manager. For example, in lazy you can use 23 | 24 | ``` 25 | ... 26 | { 27 | "geg2102/nvim-python-repl", 28 | dependencies = "nvim-treesitter", 29 | ft = {"python", "lua", "scala"}, 30 | config = function() 31 | require("nvim-python-repl").setup({ 32 | execute_on_send = false, 33 | vsplit = false, 34 | }) 35 | end 36 | } 37 | ... 38 | 39 | ``` 40 | 41 | Somewhere in your init.lua/init.vim you should place 42 | 43 | ``` require("nvim-python-repl").setup() ``` 44 | 45 | ### Keymaps 46 | 47 | 48 | There are a few keybindings that the user needs to set up. 49 | 50 | ```[lua] 51 | vim.keymap.set("n", [your keymap], function() require('nvim-python-repl').send_statement_definition() end, { desc = "Send semantic unit to REPL"}) 52 | 53 | vim.keymap.set("v", [your keymap], function() require('nvim-python-repl').send_visual_to_repl() end, { desc = "Send visual selection to REPL"}) 54 | 55 | vim.keymap.set("n", [your keyamp], function() require('nvim-python-repl').send_buffer_to_repl() end, { desc = "Send entire buffer to REPL"}) 56 | 57 | vim.keymap.set("n", [your keymap], function() require('nvim-python-repl').toggle_execute() end, { desc = "Automatically execute command in REPL after sent"}) 58 | 59 | vim.keymap.set("n", [your keymap], function() require('nvim-python-repl').toggle_vertical() end, { desc = "Create REPL in vertical or horizontal split"}) 60 | 61 | vim.keymap.set("n", [your keymap], function() require('nvim-python-repl').open_repl() end, { desc = "Opens the REPL in a window split"}) 62 | ``` 63 | 64 | ### Bonus 65 | 66 | - This plugin also works with [nvim-jupyter-client](https://github.com/geg2102/nvim-jupyter-client). There is a function to send a cell under cursor to repl. 67 | ```[lua] 68 | vim.keymap.set("n", [your keymap], function() require('nvim-python-repl').send_current_cell_to_repl() end, { desc = "Sends the cell under cursor to repl"}) 69 | ``` 70 | 71 | ### Options 72 | There are a few options. First, whether to execute the given expression on send 73 | and second, whether to send to a vertical split. By default these are set to true. Toggle on send 74 | can be toggled with `e` or `:ToggleExecuteOnSend`. Whether to send to vertical 75 | by default can be changed with `:ReplToggleVertical` or `:lua 76 | require("nvim-python-repl").toggle_vertical()`. 77 | 78 | 79 | There is an also an option to specify which spawn command you want to use for a given repl (passed as table),as well as an option to prompt from the command that starts the REPL. 80 | 81 | Here is the default setup: 82 | 83 | ``` 84 | require("nvim-python-repl").setup({ 85 | execute_on_send=false, 86 | vsplit=false, 87 | prompt_spawn=false, 88 | spawn_command={ 89 | python="ipython", 90 | scala="sbt console", 91 | lua="ilua" 92 | } 93 | }) 94 | ``` 95 | 96 | -------------------------------------------------------------------------------- /doc/nvim-python-repl.txt: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | *nvim-python-repl.nvim* 3 | nvim-python-repl is a simple plugin for sending commands to a python repl. It 4 | leverages treesitter to send statements, functions definitions, class 5 | definitions, etc. to an ipython REPL. 6 | 7 | *send_statement_definition()* 8 | send_statement_definition() 9 | 10 | Function to send the appropriate semantic unit to the REPL (will open new 11 | REPL if none open). 12 | 13 | *send_visual_to_repl()* 14 | send_visual_to_repl() 15 | 16 | Function to send visual selectin to REPL (will open new REPL if none 17 | open). 18 | 19 | *send_buffer_to_repl()* 20 | send_buffer_to_repl() 21 | 22 | Function to send entire buffer to REPL (will open new REPL if none open). 23 | 24 | *open_repl()* 25 | open_repl() 26 | Function that opens the REPL according to settings. 27 | 28 | *toggle_execute()* 29 | toggle_execute() 30 | 31 | Function that toggles whether to automatically execute on send. If on, the 32 | selection sent will automatically execute. 33 | 34 | *toggle_vertical* 35 | toggle_vertical() 36 | 37 | Function that toggles whether the REPL should open on vertical or 38 | horizontal split. 39 | 40 | *toggle_prompt()* 41 | toggle_prompt() 42 | Function that toggles whether neovim will prompt for the command that spawns 43 | the REPL rather than use the deafult one. 44 | 45 | ================================================================================ 46 | vim:tw=78:ts=8:ft=help:norl: 47 | -------------------------------------------------------------------------------- /lua/nvim-python-repl/config.lua: -------------------------------------------------------------------------------- 1 | local defaults = { 2 | execute_on_send = true, 3 | vsplit = true, 4 | prompt_spawn = false, 5 | spawn_command = { 6 | python = "ipython", 7 | scala = "sbt console", 8 | lua = "ilua", 9 | } 10 | } 11 | 12 | local function set(_, key, value) 13 | defaults[key] = value 14 | end 15 | 16 | local function get(_, key) 17 | return defaults[key] 18 | end 19 | 20 | return { 21 | defaults = defaults, 22 | get = get, 23 | set = set 24 | } 25 | -------------------------------------------------------------------------------- /lua/nvim-python-repl/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local repl = require("nvim-python-repl.nvim-python-repl") 3 | local config = require("nvim-python-repl.config") 4 | 5 | function M.setup(options) 6 | setmetatable(M, { 7 | __newindex = config.set, 8 | __index = config.get 9 | }) 10 | if options ~= nil then 11 | for k1, v1 in pairs(options) do 12 | if (type(config.defaults[k1]) == "table") then 13 | for k2, v2 in pairs(options[k1]) do 14 | config.defaults[k1][k2] = v2 15 | end 16 | else 17 | config.defaults[k1] = v1 18 | end 19 | end 20 | end 21 | end 22 | 23 | function M.send_current_cell_to_repl() 24 | repl.send_current_cell_to_repl(M) 25 | end 26 | 27 | function M.send_statement_definition() 28 | repl.send_statement_definition(M) 29 | end 30 | 31 | function M.send_visual_to_repl() 32 | vim.cmd('execute "normal \\"') 33 | repl.send_visual_to_repl(M) 34 | end 35 | 36 | function M.send_buffer_to_repl() 37 | repl.send_buffer_to_repl(M) 38 | end 39 | 40 | function M.toggle_execute() 41 | local original = config.defaults["execute_on_send"] 42 | config.defaults["execute_on_send"] = not original 43 | print("execute_on_send=" .. tostring(not original)) 44 | end 45 | 46 | function M.toggle_vertical() 47 | local original = config.defaults["vsplit"] 48 | config.defaults["vsplit"] = not original 49 | print("vsplit=" .. tostring(not original)) 50 | end 51 | 52 | function M.toggle_prompt() 53 | local original = config.defaults["prompt_spawn"] 54 | config.defaults["prompt_spawn"] = not original 55 | print("Spawn prompt=" .. tostring(not original)) 56 | end 57 | 58 | function M.open_repl() 59 | repl.open_repl(M) 60 | end 61 | 62 | return M 63 | -------------------------------------------------------------------------------- /lua/nvim-python-repl/nvim-python-repl.lua: -------------------------------------------------------------------------------- 1 | local ts_utils = require("nvim-treesitter.ts_utils") 2 | local api = vim.api 3 | 4 | M = {} 5 | 6 | M.term = { 7 | opened = 0, 8 | winid = nil, 9 | bufid = nil, 10 | chanid = nil, 11 | } 12 | 13 | -- HELPERS 14 | local visual_selection_range = function() 15 | local _, start_row, start_col, _ = unpack(vim.fn.getpos("'<")) 16 | local _, end_row, end_col, _ = unpack(vim.fn.getpos("'>")) 17 | if start_row < end_row or (start_row == end_row and start_col <= end_col) then 18 | return start_row - 1, start_col - 1, end_row - 1, end_col 19 | else 20 | return end_row - 1, end_col - 1, start_row - 1, start_col 21 | end 22 | end 23 | 24 | local get_statement_definition = function(filetype) 25 | local node = ts_utils.get_node_at_cursor() 26 | if (node:named() == false) then 27 | error("Node not recognized. Check to ensure treesitter parser is installed.") 28 | end 29 | if filetype == "python" or filetype == "scala" then 30 | while ( 31 | string.match(node:sexpr(), "import") == nil and 32 | string.match(node:sexpr(), "statement") == nil and 33 | string.match(node:sexpr(), "definition") == nil and 34 | string.match(node:sexpr(), "call_expression") == nil) do 35 | node = node:parent() 36 | end 37 | elseif filetype == "lua" then 38 | while ( 39 | string.match(node:sexpr(), "for_statement") == nil and 40 | string.match(node:sexpr(), "if_statement") == nil and 41 | string.match(node:sexpr(), "while_statement") == nil and 42 | string.match(node:sexpr(), "assignment_statement") == nil and 43 | string.match(node:sexpr(), "function_definition") == nil and 44 | string.match(node:sexpr(), "function_call") == nil and 45 | string.match(node:sexpr(), "local_declaration") == nil 46 | ) do 47 | node = node:parent() 48 | end 49 | end 50 | return node 51 | end 52 | 53 | local term_open = function(filetype, config) 54 | local orig_win = vim.api.nvim_get_current_win() 55 | if M.term.chanid ~= nil then return end 56 | if config.vsplit then 57 | api.nvim_command('vsplit') 58 | else 59 | api.nvim_command('split') 60 | end 61 | local buf = vim.api.nvim_create_buf(true, true) 62 | local win = vim.api.nvim_get_current_win() 63 | vim.api.nvim_win_set_buf(win, buf) 64 | local choice = '' 65 | if config.prompt_spawn then 66 | choice = vim.fn.input("REPL spawn command: ") 67 | else 68 | if filetype == 'scala' then 69 | choice = config.spawn_command.scala 70 | elseif filetype == 'python' then 71 | choice = config.spawn_command.python 72 | elseif filetype == 'lua' then 73 | choice = config.spawn_command.lua 74 | end 75 | end 76 | local chan = vim.fn.termopen(choice, { 77 | on_exit = function() 78 | M.term.chanid = nil 79 | M.term.opened = 0 80 | M.term.winid = nil 81 | M.term.bufid = nil 82 | end 83 | }) 84 | M.term.chanid = chan 85 | vim.bo.filetype = 'term' 86 | 87 | -- Block until terminal is ready 88 | local timeout = 5000 -- 5 seconds timeout 89 | local interval = 100 -- Check every 100ms 90 | local success = vim.wait(timeout, function() 91 | -- Check if terminal buffer has content 92 | local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) 93 | return #lines > 0 and lines[1] ~= "" 94 | end, interval) 95 | 96 | if not success then 97 | vim.notify("Terminal initialization timed out", vim.log.levels.WARN) 98 | end 99 | 100 | -- Additional wait for safety 101 | vim.wait(20) 102 | 103 | M.term.opened = 1 104 | M.term.winid = win 105 | M.term.bufid = buf 106 | -- Return to original window 107 | api.nvim_set_current_win(orig_win) 108 | end 109 | 110 | -- CONSTRUCTING MESSAGE 111 | local construct_message_from_selection = function(start_row, start_col, end_row, end_col) 112 | local bufnr = api.nvim_get_current_buf() 113 | if start_row ~= end_row then 114 | local lines = api.nvim_buf_get_lines(bufnr, start_row, end_row + 1, false) 115 | lines[1] = string.sub(lines[1], start_col + 1) 116 | -- end_row might be just after the last line. In this case the last line is not truncated. 117 | if #lines == end_row - start_row then 118 | lines[#lines] = string.sub(lines[#lines], 1, end_col) 119 | end 120 | return lines 121 | else 122 | local line = api.nvim_buf_get_lines(bufnr, start_row, start_row + 1, false)[1] 123 | -- If line is nil then the line is empty 124 | return line and { string.sub(line, start_col + 1, end_col) } or {} 125 | end 126 | end 127 | 128 | local construct_message_from_buffer = function() 129 | local bufnr = api.nvim_get_current_buf() 130 | local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false) 131 | return lines 132 | end 133 | 134 | local construct_message_from_node = function(filetype) 135 | local node = get_statement_definition(filetype) 136 | local bufnr = api.nvim_get_current_buf() 137 | local message = vim.treesitter.get_node_text(node, bufnr) 138 | if filetype == "python" then 139 | -- For Python, we need to preserve the original indentation 140 | local start_row, start_column, end_row, _ = node:range() 141 | if vim.fn.has('win32') == 1 then 142 | local lines = api.nvim_buf_get_lines(bufnr, start_row, end_row + 1, false) 143 | message = table.concat(lines, api.nvim_replace_termcodes("", true, false, true)) 144 | end 145 | -- For Linux, remove superfluous indentation so nested code is not indented 146 | while start_column ~= 0 do 147 | -- For empty blank lines 148 | message = string.gsub(message, "\n\n+", "\n") 149 | -- For nested indents in classes/functions 150 | message = string.gsub(message, "\n%s%s%s%s", "\n") 151 | start_column = start_column - 4 152 | end 153 | -- end 154 | end 155 | return message 156 | end 157 | 158 | local send_message = function(filetype, message, config) 159 | if M.term.opened == 0 then 160 | term_open(filetype, config) 161 | end 162 | local line_count = vim.api.nvim_buf_line_count(M.term.bufid) 163 | vim.api.nvim_win_set_cursor(M.term.winid, { line_count, 0 }) 164 | vim.wait(50) 165 | if filetype == "python" or filetype == "lua" then 166 | -- if vim.fn.has('win32') == 1 then 167 | -- message = message .. "\r\n" 168 | -- else 169 | message = api.nvim_replace_termcodes("[200~" .. message .. "[201~", true, false, true) 170 | -- end 171 | api.nvim_chan_send(M.term.chanid, message) 172 | elseif filetype == "scala" then 173 | if config.spawn_command.scala == "sbt console" then 174 | message = api.nvim_replace_termcodes(":paste" .. message .. "", true, false, true) 175 | else 176 | message = api.nvim_replace_termcodes("{" .. message .. "}", true, false, true) 177 | end 178 | api.nvim_chan_send(M.term.chanid, message) 179 | end 180 | if config.execute_on_send then 181 | vim.wait(20) 182 | if vim.fn.has('win32') == 1 then 183 | vim.wait(20) 184 | -- For Windows, simulate pressing Enter 185 | api.nvim_chan_send(M.term.chanid, api.nvim_replace_termcodes("", true, false, true)) 186 | else 187 | api.nvim_chan_send(M.term.chanid, "\r\r") 188 | end 189 | end 190 | end 191 | 192 | -- Function to identify cell boundaries 193 | local get_current_cell_range = function() 194 | local bufnr = vim.api.nvim_get_current_buf() 195 | local cursor_row = vim.api.nvim_win_get_cursor(0)[1] - 1 196 | 197 | local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) 198 | local start_row = 0 199 | local end_row = #lines - 1 200 | 201 | for i = cursor_row, 0, -1 do 202 | if string.match(lines[i + 1], "^# %%%%") then 203 | start_row = i + 1 204 | break 205 | end 206 | end 207 | 208 | for i = cursor_row + 1, #lines - 1 do 209 | if string.match(lines[i + 1], "^# %%%%") then 210 | end_row = i - 1 211 | break 212 | end 213 | end 214 | 215 | return start_row, end_row 216 | end 217 | 218 | -- Function to extract cell content 219 | local construct_message_from_cell = function() 220 | local start_row, end_row = get_current_cell_range() 221 | local bufnr = vim.api.nvim_get_current_buf() 222 | local lines = vim.api.nvim_buf_get_lines(bufnr, start_row, end_row + 1, false) 223 | return lines 224 | end 225 | 226 | -- Function to send current cell to REPL 227 | M.send_current_cell_to_repl = function(config) 228 | local filetype = vim.bo.filetype 229 | local message_lines = construct_message_from_cell() 230 | local message = table.concat(message_lines, "\n") 231 | send_message(filetype, message, config) 232 | end 233 | 234 | M.send_statement_definition = function(config) 235 | local filetype = vim.bo.filetype 236 | local message = construct_message_from_node(filetype) 237 | send_message(filetype, message, config) 238 | end 239 | 240 | M.send_visual_to_repl = function(config) 241 | local filetype = vim.bo.filetype 242 | local start_row, start_col, end_row, end_col = visual_selection_range() 243 | local message = construct_message_from_selection(start_row, start_col, end_row, end_col) 244 | local concat_message = "" 245 | if vim.fn.has('win32') == 1 then 246 | concat_message = table.concat(message, "") 247 | else 248 | concat_message = table.concat(message, "\n") 249 | end 250 | send_message(filetype, concat_message, config) 251 | end 252 | 253 | M.send_buffer_to_repl = function(config) 254 | local filetype = vim.bo.filetype 255 | local message = construct_message_from_buffer() 256 | local concat_message = "" 257 | if vim.fn.has('win32') == 1 then 258 | concat_message = table.concat(message, "") 259 | else 260 | concat_message = table.concat(message, "\n") 261 | end 262 | send_message(filetype, concat_message, config) 263 | end 264 | 265 | M.open_repl = function(config) 266 | local filetype = vim.bo.filetype 267 | term_open(filetype, config) 268 | end 269 | 270 | return M 271 | -------------------------------------------------------------------------------- /plugin/nvim-python-repl.vim: -------------------------------------------------------------------------------- 1 | if !has('nvim') 2 | echohl Error 3 | echom 'This plugin only works with Neovim' 4 | echohl clear 5 | finish 6 | endif 7 | 8 | " The send statement/definition command. 9 | command! SendPyObject lua require("nvim-python-repl").send_statement_definition() 10 | command! SendPySelection lua require("nvim-python-repl").send_visual_to_repl() 11 | command! SendPyBuffer lua require("nvim-python-repl").send_buffer_to_repl() 12 | command! ToggleExecuteOnSend lua require("nvim-python-repl").toggle_execute() 13 | command! ReplToggleVertical lua require("nvim-python-repl").toggle_vertical() 14 | command! ReplTogglePrompt lua require("nvim-python-repl").toggle_prompt() 15 | command! ReplOpen lua require("nvim-python-repl").open_repl() 16 | command! SendCell lua require("nvim-python-repl").send_current_cell_to_repl() 17 | " Remove default mappings 18 | " nnoremap n :SendPyObject 19 | " nnoremap e :ToggleExecuteOnSend 20 | " nnoremap nr :SendPyBuffer 21 | " vnoremap n :SendPySelection 22 | --------------------------------------------------------------------------------