diff --git a/CHANGELOG.md b/CHANGELOG.md index 91d58da..101c2d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed + +- Looking up completions now works even if there is a running computational task in the REPL ([#123](https://github.com/gcv/julia-snail/issues/123)). + + ### Added - Support for Eat as an alternative to vterm. +- `julia-snail-interrupt-task`: a new interface function which allows interrupting a running computational task in the Julia REPL ([#104](https://github.com/gcv/julia-snail/issues/104)). ### Changed diff --git a/JuliaSnail.jl b/JuliaSnail.jl index 23824e9..a04d708 100644 --- a/JuliaSnail.jl +++ b/JuliaSnail.jl @@ -815,6 +815,28 @@ end end + ### --- task handling code + +module Tasks + +import Printf + +active_tasks_lock = ReentrantLock() +active_tasks = Dict{String, Task}() + +function interrupt(reqid) + if haskey(active_tasks, reqid) + task = active_tasks[reqid] + schedule(task, InterruptException(), error=true) + return Printf.@sprintf("Interrupt scheduled for Julia reqid %s", reqid) + else + return Printf.@sprintf("Unknown reqid %s on the Julia side", reqid) + end +end + +end + + ### --- server code running = false @@ -869,31 +891,64 @@ function start(port=10011; addr="127.0.0.1") @async while Sockets.isopen(client) && !eof(client) command = readline(client, keep=true) input = nothing + expr = nothing + current_reqid = nothing try input = eval(Meta.parse(command)) expr = Meta.parse(input.code) - result = eval_in_module(input.ns, expr) - # report successful evaluation back to client + current_reqid = input.reqid + catch err + # probably a parsing error resp = elexpr([ - Symbol("julia-snail--response-success"), + Symbol("julia-snail--response-failure"), input.reqid, - result + sprint(showerror, err), + tuple(string.(stacktrace(catch_backtrace()))...) ]) send_to_client(resp, client) - catch err + continue + end + active_task = @task begin # process input try + result = eval_in_module(input.ns, expr) + # report successful evaluation back to client resp = elexpr([ - Symbol("julia-snail--response-failure"), + Symbol("julia-snail--response-success"), input.reqid, - sprint(showerror, err), - tuple(string.(stacktrace(catch_backtrace()))...) + result ]) send_to_client(resp, client) - catch err2 - # internal Snail error or unexpected IO behavior..? - println("JuliaSnail: something broke: ", sprint(showerror, err2)) - end + catch err + if isa(err, InterruptException) + resp = elexpr([ + Symbol("julia-snail--response-interrupt"), + input.reqid + ]) + send_to_client(resp, client) + else + try + resp = elexpr([ + Symbol("julia-snail--response-failure"), + input.reqid, + sprint(showerror, err), + tuple(string.(stacktrace(catch_backtrace()))...) + ]) + send_to_client(resp, client) + catch err2 + # internal Snail error or unexpected IO behavior..? + println(stderr, "JuliaSnail: something broke in reqid: ", input.reqid, "; ", sprint(showerror, err2)) + end + end + finally + lock(Tasks.active_tasks_lock) do + delete!(Tasks.active_tasks, current_reqid) + end + end # process input + end + lock(Tasks.active_tasks_lock) do + Tasks.active_tasks[current_reqid] = active_task end + schedule(active_task) end # async while loop for client connection end close(server_socket) diff --git a/README.md b/README.md index c5243f9..4e75317 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ https://user-images.githubusercontent.com/10327/128589405-7368bb50-0ef3-4003-b5d ## Installation -Julia versions >1.2.0 should all work with Snail. +Julia versions >1.6.0 should all work with Snail. Snail’s Julia-side dependencies will automatically be installed when it starts, and will stay out of your way using Julia’s [`LOAD_PATH` mechanism](https://docs.julialang.org/en/v1/base/constants/#Base.LOAD_PATH). @@ -229,6 +229,8 @@ In addition, most `xref` commands are available (except `xref-find-references`). Completion also works. Emacs built-in completion features, as well as `company-complete`, will do a reasonable job of finding the right completions in the context of the current module (though will not pick up local variables). Completion is current-module aware. +**Experimental feature:** To interrupt a Julia task started from the Emacs side (e.g. a long-running computation started with `julia-snail-send-line`), use `julia-snail-interrupt-task`. When only one task is running, Snail will simply try to terminate it. With multiple tasks, the user will be prompted for a request ID. _This is currently an opaque identifier, and the interface will be improved in the future._ + ### Multiple Julia versions diff --git a/julia-snail.el b/julia-snail.el index 4fb30b0..476084e 100644 --- a/julia-snail.el +++ b/julia-snail.el @@ -1139,6 +1139,10 @@ evaluated in the context of MODULE." (funcall callback-failure request-info)))) (julia-snail--response-base reqid)) +(defun julia-snail--response-interrupt (reqid) + "Snail task interruption response handler for REQID." + (julia-snail--response-base reqid)) + ;;; --- CST parser interface @@ -1889,6 +1893,31 @@ autocompletion aware of the available modules." (message "Caches updated: parent module %s" (julia-snail--construct-module-path module)))) +(defun julia-snail-interrupt-task () + "Try to interrupt a Julia computation task which was started on the Emacs side." + (interactive) + (let* ((running-reqids (hash-table-keys julia-snail--requests)) + ;; TODO: Filter these reqids for valid ones (e.g. ones with valid REPL buffers?) + (reqid (cond ((= 0 (length running-reqids)) + (message "No Julia tasks currently running (that the Emacs side knows about)") + nil) + ((= 1 (length running-reqids)) + (-first-item running-reqids)) + (t + (completing-read "Select id of Julia request to interrupt" + ;; TODO: This needs to include metadata and annotations for what + ;; computational task each reqid corresponds to (like a line number + ;; or the actual code). + running-reqids))))) + (when reqid + (let ((repl-buf (get-buffer julia-snail-repl-buffer))) + (message + (julia-snail--send-to-server + '("JuliaSnail" "Tasks") + (format "interrupt(\"%s\")" reqid) + :repl-buf repl-buf + :async nil)))))) + ;;; --- keymaps