Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Ultimately, `Neopyter` can control `Juppyter lab`. `Neopyter` can implement abil

- 📔JupyterLab >= 4.0.0
- ✌️ Neovim latest stable version or nightly version
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR switches the runtime async implementation away from plenary and relies on newer built-in Neovim primitives (e.g. vim._async, vim.uv, vim.pack in scripts). The README currently says “latest stable or nightly” but doesn’t state a minimum Neovim version required for these APIs; please document the minimum supported Neovim version so users know what they need.

Suggested change
- ✌️ Neovim latest stable version or nightly version
- ✌️ Neovim >= 0.11.0 (latest stable or nightly recommended)

Copilot uses AI. Check for mistakes.
- 👍`nvim-lua/plenary.nvim`
- 🌲`tree-sitter` parser of `python` and `r`
- 🤏`AbaoFromCUG/websocket.nvim` (optional for `mode="direct"`)

## JupyterLab Extension
Expand Down Expand Up @@ -186,8 +186,6 @@ Configure `JupyterLab` in side panel
{
"SUSTech-data/neopyter",
dependencies = {
'nvim-lua/plenary.nvim',
'nvim-treesitter/nvim-treesitter', -- neopyter don't depend on `nvim-treesitter`, but does depend on treesitter parser of python
'AbaoFromCUG/websocket.nvim', -- for mode='direct'
},

Expand Down
1 change: 0 additions & 1 deletion example/repro.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ require("lazy.minit").repro({
lazy = false,
dir = "../",
dependencies = {
"nvim-lua/plenary.nvim",
"AbaoFromCUG/websocket.nvim", -- for mode='direct'
},
---@type neopyter.Option
Expand Down
1 change: 0 additions & 1 deletion lua-tests/manual/repro-cmp.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ vim.opt.packpath:append(vim.fs.joinpath(vim.fn.stdpath("data"), "site"))
local __project_root__ = vim.fs.dirname(vim.fs.dirname(vim.fs.dirname(debug.getinfo(1).source:sub(2))))

vim.pack.add {
"https://github.com/nvim-lua/plenary.nvim",
"https://github.com/AbaoFromCUG/websocket.nvim",

"https://github.com/mason-org/mason.nvim",
Expand Down
5 changes: 2 additions & 3 deletions lua/neopyter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ local jupyter = require("neopyter.jupyter")
local JupyterLab = require("neopyter.jupyter.jupyterlab")
local PercentParser = require("neopyter.parser.percent")
local a = require("neopyter.async")
local api = a.api

---@class neopyter.Option
---@field remote_address? string
Expand All @@ -29,7 +28,7 @@ local neopyter = {
delay_setup_done = false
}

local is_windows = vim.loop.os_uname().version:match("Windows")
local is_windows = vim.uv.os_uname().version:match("Windows")

---@nodoc
---@doc-capture default-config
Expand Down Expand Up @@ -99,7 +98,7 @@ function neopyter.setup(config)

if neopyter.config.auto_attach then
local augroup = vim.api.nvim_create_augroup("neopyter", { clear = true })
api.nvim_create_autocmd({ "BufReadPost" }, {
a.api.nvim_create_autocmd({ "BufReadPost" }, {
group = augroup,
pattern = neopyter.config.file_pattern,
callback = function()
Expand Down
254 changes: 206 additions & 48 deletions lua/neopyter/async.lua
Original file line number Diff line number Diff line change
@@ -1,42 +1,208 @@
local a = require("plenary.async")

local async = {
run = a.run,

---run function in async context, until timeout or complete
---@param suspend_fn fun()
---@param callback fun(success, ret_val)
---@param timeout number?
run_blocking = function(suspend_fn, callback, timeout)
local resolved = false
local msg
vim.schedule(function()
a.run(suspend_fn, function(data)
msg = data
resolved = true
end)
local a = require("vim._async")



--- @brief Neopyter's async module, which provides async functions and utilities for Neopyter.
--- Which is built on top of native async support (`vim._async`) in Neovim, and provides a more convenient API for users to use async functions in Neopyter.
--- The API of neopyter are mostly async, and users could call them in a sync context, neopyter will automatically wrap them in an async context, so
--- that users could call them directly without worrying about async context, but the order of execution is not guaranteed, if you want to guarantee
--- the order of execution, you should use `require("neopyter.async").run(...)` to run them in an async context.
Comment on lines +1 to +9
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require("vim._async") depends on Neovim’s internal (underscore-prefixed) API and may not be available/stable across versions. Consider guarding this with pcall and providing a clear error (or fallback implementation) so users on unsupported Neovim versions get an actionable message.

Copilot uses AI. Check for mistakes.
---
--- Example1: Call API in sync context, but the order of execution is not guaranteed
--- ```lua
--- vim.defer_fn(function()
--- -- non-async context, API response may be unordered
--- current_notebook:run_selected_cell()
--- current_notebook:run_all_above()
--- current_notebook:run_all_below()
--- end, 0)
--- Example2: Call API in async context, the order of execution is guaranteed
--- require("neopyter.async").run(function()
--- -- async context, so which will call and return in order
--- current_notebook:run_selected_cell()
--- current_notebook:run_all_above()
--- current_notebook:run_all_below()
--- end)
--- ```

local async = {}

---Creates an async function with a callback style function.
---@param func function: A callback style function to be converted. The last argument must be the callback.
---@param argc number: The number of arguments of func. Must be included.
---@return async fun: Returns an async function
function async.wrap(func, argc)
---@async
return function(...)
return a.await(argc, func, ...)
end
end

async.scheduler = async.wrap(vim.schedule, 1)

function async.safe_async()
if vim.in_fast_event() then
async.scheduler()
end
end

---Use this to either run a future concurrently and then do something else
---or use it to run a future with a callback in a non async context
---@param func async fun(): ...:any
---@param on_finish? fun(err: string?, ...:any)
function async.run(func, on_finish)
if on_finish == nil then
on_finish = function(err)
if err then
error(err)
end
end
end
a.run(func, on_finish)
end

---run function in async context, until timeout or complete
---@param suspend_fn fun()
---@param on_finish? fun(err: string?, ...:any)
---@param timeout number?
function async.run_blocking(suspend_fn, on_finish, timeout)
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async.run_blocking documents on_finish as optional, but the implementation unconditionally calls it, which will error if it’s omitted. Either make on_finish required (update annotations) or provide a default callback like async.run does.

Suggested change
function async.run_blocking(suspend_fn, on_finish, timeout)
function async.run_blocking(suspend_fn, on_finish, timeout)
if on_finish == nil then
on_finish = function(err)
if err then
error(err)
end
end
end

Copilot uses AI. Check for mistakes.
if not on_finish then
on_finish = function(err)
if err then
error(err)
end
end
end

local resolved = false
local err
local data
vim.schedule(function()
async.run(suspend_fn, function(e, ...)
if e == nil then
data = { ... }
else
err = e
end
resolved = true
end)
end)

local success = vim.wait(timeout or 10000, function()
return resolved
end, 100)
callback(success, msg)
end,
local success = vim.wait(timeout or 10000, function()
return resolved
end, 100)
if not success then
on_finish("Async function timed out", unpack(data or {}))
else
on_finish(err, unpack(data or {}))
end
end

safe_async = function()
if vim.in_fast_event() then
require("plenary.async.util").scheduler()
async.fn = vim.fn
async.fn = setmetatable({}, {
__index = function(_, k)
return function(...)
-- if we are in a fast event await the scheduler
async.safe_async()
return vim.fn[k](...)
end
end,
}
})

async.uv = vim.uv
async.api = a.uv
async.uv = {}

local function add(name, argc, custom)
local success, ret = pcall(async.wrap, custom or vim.uv[name], argc)

if not success then
error("Failed to add function with name " .. name)
end

async.uv[name] = ret
end


add("close", 4) -- close a handle

-- filesystem operations
add("fs_open", 4)
add("fs_read", 4)
add("fs_close", 2)
add("fs_unlink", 2)
add("fs_write", 4)
add("fs_mkdir", 3)
add("fs_mkdtemp", 2)
-- 'fs_mkstemp',
add("fs_rmdir", 2)
add("fs_scandir", 2)
add("fs_stat", 2)
add("fs_fstat", 2)
add("fs_lstat", 2)
add("fs_rename", 3)
add("fs_fsync", 2)
add("fs_fdatasync", 2)
add("fs_ftruncate", 3)
add("fs_sendfile", 5)
add("fs_access", 3)
add("fs_chmod", 3)
add("fs_fchmod", 3)
add("fs_utime", 4)
add("fs_futime", 4)
-- 'fs_lutime',
add("fs_link", 3)
add("fs_symlink", 4)
add("fs_readlink", 2)
add("fs_realpath", 2)
add("fs_chown", 4)
add("fs_fchown", 4)
-- 'fs_lchown',
add("fs_copyfile", 4)
add("fs_opendir", 3, function(path, entries, callback)
return uv.fs_opendir(path, callback, entries)
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fs_opendir wrapper uses an undefined uv identifier, so this module will error on load. Use vim.uv (or a local uv = vim.uv) and keep the argument order consistent with vim.uv.fs_opendir’s signature.

Suggested change
return uv.fs_opendir(path, callback, entries)
return vim.uv.fs_opendir(path, entries, callback)

Copilot uses AI. Check for mistakes.
end)
add("fs_readdir", 2)
add("fs_closedir", 2)
-- 'fs_statfs',

-- stream
add("shutdown", 2)
add("listen", 3)
-- add('read_start', 2) -- do not do this one, the callback is made multiple times
add("write", 3)
add("write2", 4)
add("shutdown", 2)
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add("shutdown", 2) is listed twice in the stream section, which is redundant and makes the wrapper list harder to maintain. Remove the duplicate entry (or replace it with the intended missing wrapper if this was a copy/paste).

Suggested change
add("shutdown", 2)

Copilot uses AI. Check for mistakes.

-- tcp
add("tcp_connect", 4)
-- 'tcp_close_reset',

-- pipe
add("pipe_connect", 3)

-- udp
add("udp_send", 5)
add("udp_recv_start", 2)

-- fs event (wip make into async await event)
-- fs poll event (wip make into async await event)

-- dns
add("getaddrinfo", 4)
add("getnameinfo", 2)

-- timer
add("new_timer", 0)


async.api = vim.api
async.api = setmetatable({}, {
__index = function(_, k)
return a.api[k]
return function(...)
-- if we are in a fast event await the scheduler
async.safe_async()
return vim.api[k](...)
end
end,
})

Expand All @@ -49,33 +215,24 @@ async.api.nvim_create_autocmd = function(event, opts)
local callback = opts.callback
opts.callback = function(...)
local args = { ... }
a.run(function()
async.run(function()
callback(unpack(args))
end, function() end)
end, function(err)
if err then
error(err)
end
end)
end
end
-- vim.print(event)
-- vim.print(opts)

return a.api.nvim_create_autocmd(event, opts)
return vim.api.nvim_create_autocmd(event, opts)
end

async.fn = vim.fn
async.fn = setmetatable({}, {
__index = function(_, k)
return function(...)
-- if we are in a fast event await the scheduler
async.safe_async()
return vim.fn[k](...)
end
end,
})

async.defer_fn = vim.defer_fn

async.defer_fn = function(fn, timeout)
vim.defer_fn(function()
a.run(function()
async.run(function()
fn()
end, function() end)
end, timeout)
Expand All @@ -88,7 +245,7 @@ async.health = setmetatable({}, {
return function(...)
-- if we are in a fast event await the scheduler
if vim.in_fast_event() then
require("plenary.async.util").scheduler()
async.scheduler()
end
return vim.health[k](...)
end
Expand Down Expand Up @@ -124,12 +281,12 @@ function async.safe_wrap(cls, ignored_methods)
else
local params = { ... }
logger.log(string.format("Call api [%s] from main thread directly", key))
return a.run(function()
return async.run(function()
return value(unpack(params))
end, function(result)
local utils = require("neopyter.utils")
---WARN:Only when the user directly calls the API from the main thread, e.g. autocmd, keymap,
Comment on lines +284 to 288
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async.run invokes callbacks as (err, ...), but the callback passed here is function(result) so result will actually receive the error value and successful return values will be shifted/dropped. Update the callback signature to accept (err, result) (and handle err) so notifications/logging reflect the real result.

Copilot uses AI. Check for mistakes.
--- programmatic calls should be wrapped with `require("plenary.async").run(...)`
--- programmatic calls should be wrapped with `require("neopyter.async").run(...)`
utils.notify_info(string.format("Call api [%s] complete: %s", key, result))
logger.log(string.format("Call api [%s] complete from main thread directly: %s", key, result))
end)
Expand All @@ -141,4 +298,5 @@ function async.safe_wrap(cls, ignored_methods)
logger.log(string.format("inject class end", vim.inspect(cls)))
return cls
end

return async
2 changes: 1 addition & 1 deletion lua/neopyter/blink.lua
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function neopyter:get_completions(context, callback)
items = items,
context = context,
})
end, function() end)
end)
end

return neopyter
2 changes: 1 addition & 1 deletion lua/neopyter/cmp.lua
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function neopyter:complete(params, callback)
end)
:totable()
callback(items)
end, function() end)
end)
end

return neopyter
Loading
Loading