├── LICENSE ├── Manifest.toml ├── Project.toml ├── README.md ├── lua └── julia-repl.lua ├── plugin └── julia-repl.vim └── src └── REPLVim.jl /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Andrey Popp and contributors 4 | Copyright (c) 2021 Chris Foster and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Manifest.toml: -------------------------------------------------------------------------------- 1 | # This file is machine-generated - editing it directly is not advised 2 | 3 | julia_version = "1.7.2" 4 | manifest_format = "2.0" 5 | 6 | [[deps.Base64]] 7 | uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" 8 | 9 | [[deps.Dates]] 10 | deps = ["Printf"] 11 | uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" 12 | 13 | [[deps.InteractiveUtils]] 14 | deps = ["Markdown"] 15 | uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" 16 | 17 | [[deps.JSON3]] 18 | deps = ["Dates", "Mmap", "Parsers", "StructTypes", "UUIDs"] 19 | git-tree-sha1 = "8c1f668b24d999fb47baf80436194fdccec65ad2" 20 | uuid = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" 21 | version = "1.9.4" 22 | 23 | [[deps.Logging]] 24 | uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" 25 | 26 | [[deps.Markdown]] 27 | deps = ["Base64"] 28 | uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" 29 | 30 | [[deps.Mmap]] 31 | uuid = "a63ad114-7e13-5084-954f-fe012c677804" 32 | 33 | [[deps.Parsers]] 34 | deps = ["Dates"] 35 | git-tree-sha1 = "1285416549ccfcdf0c50d4997a94331e88d68413" 36 | uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" 37 | version = "2.3.1" 38 | 39 | [[deps.Printf]] 40 | deps = ["Unicode"] 41 | uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" 42 | 43 | [[deps.REPL]] 44 | deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] 45 | uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" 46 | 47 | [[deps.Random]] 48 | deps = ["SHA", "Serialization"] 49 | uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" 50 | 51 | [[deps.SHA]] 52 | uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" 53 | 54 | [[deps.Serialization]] 55 | uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" 56 | 57 | [[deps.Sockets]] 58 | uuid = "6462fe0b-24de-5631-8697-dd941f90decc" 59 | 60 | [[deps.StructTypes]] 61 | deps = ["Dates", "UUIDs"] 62 | git-tree-sha1 = "d24a825a95a6d98c385001212dc9020d609f2d4f" 63 | uuid = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" 64 | version = "1.8.1" 65 | 66 | [[deps.UUIDs]] 67 | deps = ["Random", "SHA"] 68 | uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" 69 | 70 | [[deps.Unicode]] 71 | uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" 72 | -------------------------------------------------------------------------------- /Project.toml: -------------------------------------------------------------------------------- 1 | name = "REPLVim" 2 | uuid = "2a140ba1-97ad-47e0-ac3b-8e555ea992ff" 3 | authors = ["Andrey Popp <8mayday@gmail.com>"] 4 | version = "0.1.0" 5 | 6 | [deps] 7 | JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" 8 | Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" 9 | REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" 10 | Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" 11 | StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # julia-repl-vim 2 | 3 | Julia REPL plugin for vim/neovim 4 | 5 | https://user-images.githubusercontent.com/30594/166437124-f81fc776-c72c-4332-990f-10ba3b9dbaf1.mov 6 | 7 | ## Usage 8 | 9 | Install the support Julia package: 10 | ``` 11 | using Pkg 12 | Pkg.add(url="https://github.com/andreypopp/julia-repl-vim") 13 | ``` 14 | 15 | Now when you start `julia` you can do: 16 | ``` 17 | using REPLVim 18 | @async REPLVim.serve() 19 | ``` 20 | this starts a REPL server (default port 2345) the editor can connect to. 21 | 22 | to start on a specific port run: 23 | 24 | ``` 25 | @async REPLVim.serve() 26 | ``` 27 | 28 | In your `.vimrc`: 29 | ``` 30 | Plug 'andreypopp/julia-repl-vim' 31 | ``` 32 | to install the editor plugin. 33 | 34 | In editor to connect to the REPL server: 35 | ``` 36 | :JuliaREPLConnect 37 | ``` 38 | 39 | 40 | Now `e` will eval the current line or the current selection in REPL as 41 | if you typed it directly. 42 | 43 | REPL completions are also available,but not enabled by default. Enable with 44 | ``` 45 | let g:julia_repl_complete=1 46 | ``` 47 | in your vimrc 48 | 49 | 50 | The `` omni completion will query REPL for 51 | completions. 52 | 53 | 54 | ## Credits 55 | 56 | REPL server code is based on [RemoteREPL.jl] project. 57 | 58 | [RemoteREPL.jl]: https://github.com/c42f/RemoteREPL.jl 59 | -------------------------------------------------------------------------------- /lua/julia-repl.lua: -------------------------------------------------------------------------------- 1 | callbacks = {} 2 | function logerror(msg) 3 | vim.api.nvim_echo({{"julia-repl: "..msg, "ErrorMsg"}}, true, {}) 4 | end 5 | 6 | function loginfo(msg) 7 | vim.api.nvim_echo({{"julia-repl: "..msg, "InfoMsg"}}, true, {}) 8 | end 9 | 10 | function connect(opts) 11 | if opts == nil then opts = {} end 12 | local host = opts.host or "localhost" 13 | local port = opts.port or 2345 14 | local buf = {} 15 | local id = 0 16 | 17 | local on_response = function(response) 18 | data = vim.fn.json_decode(response) 19 | local callback = callbacks[data.id] 20 | if callback ~= nil then 21 | callbacks[data.id] = nil 22 | callback(data) 23 | else 24 | logerror("orphan response: "..response) 25 | end 26 | end 27 | 28 | local repl 29 | local connstr = host..":"..port 30 | ok, ch = pcall(vim.fn.sockconnect, "tcp", connstr, { 31 | on_data = function(ch, data, name) 32 | if #data == 1 and data[1] == '' then -- EOF 33 | logerror("REPL connection closed") 34 | if #buf > 0 then 35 | on_response(table.concat(buf, '')) 36 | buf = {} 37 | end 38 | if opts.on_close then 39 | opts.on_close() 40 | end 41 | else 42 | for _, chunk in ipairs(data) do 43 | if chunk == '' and #buf > 0 then 44 | on_response(table.concat(buf, '')) 45 | buf = {} 46 | else 47 | table.insert(buf, chunk) 48 | end 49 | end 50 | end 51 | end 52 | }) 53 | 54 | if not ok then 55 | return logerror("unable to connect to Julia REPL at "..connstr) 56 | end 57 | loginfo("connected to Julia REPL at "..connstr) 58 | 59 | function complete(prefix, callback) 60 | id = id + 1 61 | data = {type="complete",id=id,full=prefix,partial=prefix} 62 | callbacks[id] = callback 63 | vim.fn.chansend(ch, vim.fn.json_encode(data)) 64 | vim.fn.chansend(ch, "\n") 65 | end 66 | 67 | function eval(code, callback) 68 | id = id + 1 69 | data = {type="eval",id=id,code=code} 70 | callbacks[id] = callback 71 | vim.fn.chansend(ch, vim.fn.json_encode(data)) 72 | vim.fn.chansend(ch, "\n") 73 | end 74 | 75 | function input(code, callback) 76 | id = id + 1 77 | data = {type="input",id=id,code=code} 78 | callbacks[id] = callback 79 | vim.fn.chansend(ch, vim.fn.json_encode(data)) 80 | vim.fn.chansend(ch, "\n") 81 | end 82 | 83 | function help(symbol, callback) 84 | symbol = vim.fn.json_encode(symbol) 85 | eval("eval(REPL.helpmode("..symbol.."))", callback) 86 | end 87 | 88 | function close() 89 | vim.fn.chanclose(ch) 90 | end 91 | 92 | repl = { 93 | eval=eval, 94 | input=input, 95 | complete=complete, 96 | help=help, 97 | close=close, 98 | } 99 | return repl 100 | end 101 | 102 | function setup(po) 103 | local buf = vim.fn.bufnr() 104 | local ok, repl = pcall(vim.api.nvim_buf_get_var, buf, 'julia_repl') 105 | if ok and repl ~= nil then 106 | repl.close() 107 | end 108 | if po == nil then 109 | po = 2345 110 | end 111 | repl = connect { 112 | host="localhost", 113 | port=po, 114 | on_close=function() 115 | vim.api.nvim_buf_set_var(buf, 'julia_repl', nil) 116 | end 117 | } 118 | vim.api.nvim_buf_set_var(buf, 'julia_repl', repl) 119 | if vim.g.julia_repl_complete then 120 | vim.api.nvim_buf_set_option(0, 'omnifunc', 'v:lua.julia_repl_comp') 121 | end 122 | end 123 | 124 | function _G.julia_repl_send(code) 125 | local repl = vim.b.julia_repl 126 | if repl == nil or repl == vim.NIL then 127 | return logerror("not connected to Julia REPL (use :JuliaREPLConnect)") 128 | end 129 | repl.input(code, function() end) 130 | end 131 | 132 | function _G.julia_repl_comp(findstart, base) 133 | local repl = vim.b.julia_repl 134 | if repl == nil or repl == vim.NIL then return -2 end 135 | if findstart == 1 then 136 | local pos = vim.api.nvim_win_get_cursor(0) 137 | local line = vim.api.nvim_get_current_line() 138 | local line_to_cursor = line:sub(1, pos[2]) 139 | local prefixpos = vim.fn.match(line_to_cursor, 140 | '\\(using|import \\)?\\(\\k\\|@\\)\\(\\k\\|@\\|\\.\\)*$') 141 | if prefixpos < 0 then prefixpos = 0 end 142 | return prefixpos 143 | else 144 | local prefix = vim.fn.match(base, "\\(\\k\\|@\\)*$") 145 | local pre = "" 146 | if prefix > 1 then pre = base:sub(1, prefix) end 147 | local tick = vim.b.changedtick 148 | local items = nil 149 | repl.complete(base, function(data) 150 | if not data.ok then items = false return end 151 | if tick ~= vim.b.changedtick then items = false return end 152 | items = {} 153 | for _, item in ipairs(data.ok[1]) do 154 | table.insert(items, pre..item) 155 | end 156 | end) 157 | vim.wait(1000, function() return items ~= nil end, 100, false) 158 | if items then 159 | return {words=items, refresh=false} 160 | else 161 | return {words={}, refresh=false} 162 | end 163 | end 164 | end 165 | 166 | return {setup=setup} 167 | -------------------------------------------------------------------------------- /plugin/julia-repl.vim: -------------------------------------------------------------------------------- 1 | function! s:send_range(startline, endline) abort 2 | let rv = getreg('"') 3 | let rt = getregtype('"') 4 | silent exe a:startline . ',' . a:endline . 'yank' 5 | lua require('julia-repl') 6 | call v:lua.julia_repl_send(trim(@", "\n", 2)) 7 | call setreg('"', rv, rt) 8 | endfunction 9 | 10 | function! s:send_op(type, ...) abort 11 | let sel_save = &selection 12 | let &selection = "inclusive" 13 | let rv = getreg('"') 14 | let rt = getregtype('"') 15 | 16 | if a:0 " Invoked from Visual mode, use '< and '> marks. 17 | silent exe "normal! `<" . a:type . '`>y' 18 | elseif a:type == 'line' 19 | silent exe "normal! '[V']y" 20 | elseif a:type == 'block' 21 | silent exe "normal! `[\`]\y" 22 | else 23 | silent exe "normal! `[v`]y" 24 | endif 25 | 26 | call setreg('"', @", 'V') 27 | lua require('julia-repl') 28 | call v:lua.julia_repl_send(trim(@", "\n", 2)) 29 | 30 | let &selection = sel_save 31 | call setreg('"', rv, rt) 32 | endfunction 33 | 34 | function! s:store_cur() 35 | let s:cur = winsaveview() 36 | endfunction 37 | 38 | function! s:restore_cur() 39 | if exists("s:cur") 40 | call winrestview(s:cur) 41 | unlet s:cur 42 | endif 43 | endfunction 44 | 45 | command -range -bar -nargs=1 JuliaREPLConnect 46 | \ lua require('julia-repl').setup() 47 | command -range -bar -nargs=0 JuliaREPLSend 48 | \ call s:store_cur() 49 | \| call s:send_range(, ) 50 | \| call s:restore_cur() 51 | command -range -bar -nargs=0 JuliaREPLSendRegion 52 | \ call s:store_cur() 53 | \| call s:send_op(visualmode(), 1) 54 | \| call s:restore_cur() 55 | 56 | nmap e :JuliaREPLSend 57 | xmap e :JuliaREPLSendRegion 58 | -------------------------------------------------------------------------------- /src/REPLVim.jl: -------------------------------------------------------------------------------- 1 | module REPLVim 2 | 3 | using Sockets 4 | using Logging 5 | using REPL 6 | using JSON3 7 | using StructTypes 8 | 9 | abstract type Command end 10 | 11 | struct EvalCommand <: Command 12 | type::String 13 | id::Int32 14 | code::String 15 | end 16 | 17 | struct InputCommand <: Command 18 | type::String 19 | id::Int32 20 | code::String 21 | end 22 | 23 | struct CompleteCommand <: Command 24 | type::String 25 | id::Int32 26 | full::String 27 | partial::String 28 | end 29 | 30 | struct ExitCommand <: Command 31 | type::String 32 | id::Int32 33 | end 34 | 35 | struct InterruptCommand <: Command 36 | type::String 37 | id::Int32 38 | end 39 | 40 | StructTypes.StructType(::Type{Command}) = StructTypes.AbstractType() 41 | StructTypes.StructType(::Type{EvalCommand}) = StructTypes.Struct() 42 | StructTypes.StructType(::Type{InputCommand}) = StructTypes.Struct() 43 | StructTypes.StructType(::Type{CompleteCommand}) = StructTypes.Struct() 44 | StructTypes.StructType(::Type{ExitCommand}) = StructTypes.Struct() 45 | StructTypes.StructType(::Type{InterruptCommand}) = StructTypes.Struct() 46 | StructTypes.subtypekey(::Type{Command}) = :type 47 | StructTypes.subtypes(::Type{Command}) = 48 | (eval=EvalCommand, 49 | input=InputCommand, 50 | complete=CompleteCommand, 51 | interrupt=InterruptCommand, 52 | exit=ExitCommand) 53 | 54 | mutable struct Session 55 | socket 56 | display_properties::Dict 57 | in_module::Module 58 | end 59 | 60 | function sprint_ctx(f, session) 61 | io = IOBuffer() 62 | ctx = IOContext(io, :module=>session.in_module, session.display_properties...) 63 | f(ctx) 64 | String(take!(io)) 65 | end 66 | 67 | function execute_command(session::Session, cmd::EvalCommand) 68 | sprint_ctx(session) do io 69 | with_logger(ConsoleLogger(io)) do 70 | expr = Meta.parse(cmd.code) 71 | result = Base.eval(session.in_module, expr) 72 | if result !== nothing 73 | Base.invokelatest(show, io, MIME"text/plain"(), result) 74 | end 75 | end 76 | end 77 | end 78 | function execute_command(session::Session, cmd::InputCommand) 79 | sprint_ctx(session) do io 80 | with_logger(ConsoleLogger(io)) do 81 | repl = Base.active_repl 82 | juliamode = repl.interface.modes[1] 83 | juliamodestate = repl.mistate.mode_state[juliamode] 84 | REPL.LineEdit.edit_insert(juliamodestate, cmd.code) 85 | REPL.LineEdit.commit_line(repl.mistate) 86 | juliamode.on_done(repl.mistate, 87 | REPL.LineEdit.buffer(repl.mistate), true) 88 | end 89 | end 90 | end 91 | function execute_command(session::Session, cmd::CompleteCommand) 92 | ret, range, should_complete = REPL.completions(cmd.full, lastindex(cmd.partial), 93 | session.in_module) 94 | (unique!(map(REPL.completion_text, ret)), cmd.partial[range], should_complete) 95 | end 96 | 97 | function serve_session(session) 98 | socket = session.socket 99 | flush(socket) 100 | @sync begin 101 | commands = Channel{Command}(1) 102 | responses = Channel{Any}(1) 103 | 104 | executor = @async try 105 | while true 106 | cmd = 107 | try 108 | take!(commands) 109 | catch exc 110 | if exc isa InvalidStateException && !isopen(commands) 111 | break 112 | else 113 | rethrow() 114 | end 115 | end 116 | try 117 | result = execute_command(session, cmd) 118 | if result !== nothing 119 | put!(responses, 120 | Dict(:ok => result, :id => cmd.id)) 121 | end 122 | catch exc 123 | if exc isa InterruptException 124 | continue 125 | else 126 | put!(responses, 127 | Dict(:error => sprint(showerror, exc), :id => cmd.id)) 128 | end 129 | end 130 | end 131 | catch exc 132 | @error "RemoteREPL backend crashed" exception=exc,catch_backtrace() 133 | finally 134 | close(responses) 135 | end 136 | 137 | @async try 138 | while true 139 | response = take!(responses) 140 | JSON3.write(socket, response) 141 | write(socket, "\n") 142 | end 143 | catch exc 144 | if isopen(responses) && isopen(socket) 145 | rethrow() 146 | end 147 | finally 148 | close(socket) 149 | end 150 | 151 | try 152 | while isopen(socket) 153 | cmd = nothing 154 | try 155 | line = readline(socket) 156 | cmd = JSON3.read(line, Command) 157 | catch exc 158 | put!(responses, Dict(:error=>"invalid cmd")) 159 | continue 160 | end 161 | @debug "Client cmd" cmd 162 | if cmd isa ExitCommand 163 | break 164 | elseif cmd isa InterruptCommand 165 | schedule(executor, InterruptException(); error=true) 166 | else 167 | put!(commands, cmd) 168 | end 169 | end 170 | catch exc 171 | @error "RemoteREPL frontend crashed" exception=exc,catch_backtrace() 172 | rethrow() 173 | finally 174 | close(socket) 175 | close(commands) 176 | end 177 | end 178 | end 179 | 180 | serve(host=Sockets.localhost, port::Integer=2345; kws...) = 181 | let server = listen(host, port) 182 | try 183 | @info "REPL server on $host:$port" 184 | serve(server; kws...) 185 | finally 186 | close(server) 187 | end 188 | end 189 | 190 | serve(port::Integer; kws...) = 191 | serve(Sockets.localhost, port; kws...) 192 | 193 | function serve(server::Base.IOServer) 194 | open_sessions = Set{Session}() 195 | @sync try 196 | while isopen(server) 197 | socket = accept(server) 198 | session = Session(socket, Dict(), Main) 199 | push!(open_sessions, session) 200 | peer = getpeername(socket) 201 | @async try 202 | serve_session(session) 203 | catch exc 204 | if !(exc isa EOFError && !isopen(session.socket)) 205 | @warn "Something went wrong evaluating client command" #= 206 | =# exception=exc,catch_backtrace() 207 | end 208 | finally 209 | @info "REPL client exited" 210 | close(session.socket) 211 | pop!(open_sessions, session) 212 | end 213 | @info "REPL client connected" 214 | end 215 | catch exc 216 | if exc isa Base.IOError && !isopen(server) 217 | # Ok - server was closed 218 | return 219 | end 220 | @error "Unexpected server failure" isopen(server) exception=exc,catch_backtrace() 221 | rethrow() 222 | finally 223 | for session in open_sessions 224 | close(session.socket) 225 | end 226 | end 227 | end 228 | 229 | end 230 | --------------------------------------------------------------------------------