diff --git a/README.md b/README.md index e39b4d1..b47ab2b 100644 --- a/README.md +++ b/README.md @@ -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 - - 👍`nvim-lua/plenary.nvim` + - 🌲`tree-sitter` parser of `python` and `r` - 🤏`AbaoFromCUG/websocket.nvim` (optional for `mode="direct"`) ## JupyterLab Extension @@ -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' }, diff --git a/example/repro.lua b/example/repro.lua index 799192e..b4fde3f 100644 --- a/example/repro.lua +++ b/example/repro.lua @@ -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 diff --git a/lua-tests/manual/repro-cmp.lua b/lua-tests/manual/repro-cmp.lua index 9d313ec..6acf991 100644 --- a/lua-tests/manual/repro-cmp.lua +++ b/lua-tests/manual/repro-cmp.lua @@ -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", diff --git a/lua/neopyter.lua b/lua/neopyter.lua index 7936bfa..a890ccd 100644 --- a/lua/neopyter.lua +++ b/lua/neopyter.lua @@ -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 @@ -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 @@ -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() diff --git a/lua/neopyter/async.lua b/lua/neopyter/async.lua index 0227cc6..1bc0d81 100644 --- a/lua/neopyter/async.lua +++ b/lua/neopyter/async.lua @@ -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. +--- +--- 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) + 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) +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) + +-- 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, }) @@ -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) @@ -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 @@ -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, - --- 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) @@ -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 diff --git a/lua/neopyter/blink.lua b/lua/neopyter/blink.lua index 8499b0d..0442399 100644 --- a/lua/neopyter/blink.lua +++ b/lua/neopyter/blink.lua @@ -110,7 +110,7 @@ function neopyter:get_completions(context, callback) items = items, context = context, }) - end, function() end) + end) end return neopyter diff --git a/lua/neopyter/cmp.lua b/lua/neopyter/cmp.lua index a6e5f6e..80152b5 100644 --- a/lua/neopyter/cmp.lua +++ b/lua/neopyter/cmp.lua @@ -81,7 +81,7 @@ function neopyter:complete(params, callback) end) :totable() callback(items) - end, function() end) + end) end return neopyter diff --git a/lua/neopyter/health.lua b/lua/neopyter/health.lua index 72ce600..c18888f 100644 --- a/lua/neopyter/health.lua +++ b/lua/neopyter/health.lua @@ -86,10 +86,9 @@ function M.check() else health.info(string.format("neovim plugin(neopyter@%s) status: inactive", nvim_plugin_ver)) end - end, function(success) - if not success then - health.error(string.format("return code:%s", code)) - health.error("Call async function without return in a long time!!") + end, function(err) + if err then + health.error(string.format("error with:%s", err)) end end) end diff --git a/lua/neopyter/highlight.lua b/lua/neopyter/highlight.lua index 4d0ad5c..bbf8ed7 100644 --- a/lua/neopyter/highlight.lua +++ b/lua/neopyter/highlight.lua @@ -1,8 +1,6 @@ local utils = require("neopyter.utils") local ts = require("neopyter.treesitter") local a = require("neopyter.async") -local api = a.api -local fn = a.fn ---@class neopyter.HighlightOption ---@field enable boolean @@ -38,9 +36,10 @@ function M.setup() end end +---@async local function update_zen_highlight(buf) - api.nvim_buf_clear_namespace(0, ns_highlight, 0, -1) - api.nvim_set_hl(ns_highlight, "NeopyterDim", { link = "DiagnosticUnnecessary", default = true }) + a.api.nvim_buf_clear_namespace(0, ns_highlight, 0, -1) + a.api.nvim_set_hl(ns_highlight, "NeopyterDim", { link = "DiagnosticUnnecessary", default = true }) local notebook = require("neopyter.jupyter.jupyterlab"):get_notebook(buf) if not notebook then utils.notify_warn("Can't highlight buffer: cann't find notebook") @@ -56,7 +55,7 @@ local function update_zen_highlight(buf) local start_row = cell.start_row local end_row = cell.end_row - for i = fn.line("w0") - 1, fn.line("w$") - 1 do + for i = a.fn.line("w0") - 1, a.fn.line("w$") - 1 do if i < start_row or i > end_row then api.nvim_buf_set_extmark(0, ns_highlight, i, 0, { end_row = i + 1, @@ -83,7 +82,7 @@ local function highlight_node(node, hl_group, mode, include_whitespace, priority local start_row, start_col, end_row, end_col = unpack(range) if mode == "linewise" then - api.nvim_buf_set_extmark(0, ns_highlight, start_row, 0, { + a.api.nvim_buf_set_extmark(0, ns_highlight, start_row, 0, { -- end_line = end_row + 1, -- end_col = 0, line_hl_group = hl_group, @@ -91,7 +90,7 @@ local function highlight_node(node, hl_group, mode, include_whitespace, priority priority = priority, }) else - api.nvim_buf_set_extmark(0, ns_highlight, start_row, start_col, { + a.api.nvim_buf_set_extmark(0, ns_highlight, start_row, start_col, { end_line = end_row, end_col = end_col, hl_group = hl_group, @@ -115,6 +114,7 @@ local function update_separator_highlight(buf) end ---@nodoc +---@async function M.attach(buf, augroup) local config = require("neopyter").config.highlight local updated = false @@ -127,7 +127,7 @@ function M.attach(buf, augroup) else update_highlight = utils.throttle(update_separator_highlight, nil, buf) end - api.nvim_create_autocmd({ "BufWinEnter", "BufWritePost", "CursorMoved", "CursorMovedI", "WinScrolled" }, { + a.api.nvim_create_autocmd({ "BufWinEnter", "BufWritePost", "CursorMoved", "CursorMovedI", "WinScrolled" }, { buffer = buf, callback = update_highlight, group = augroup, diff --git a/lua/neopyter/jupyter/jupyterlab.lua b/lua/neopyter/jupyter/jupyterlab.lua index 6b9dcdf..c2539f6 100644 --- a/lua/neopyter/jupyter/jupyterlab.lua +++ b/lua/neopyter/jupyter/jupyterlab.lua @@ -1,9 +1,6 @@ local Notebook = require("neopyter.jupyter.notebook") local utils = require("neopyter.utils") local a = require("neopyter.async") -local Path = require("plenary.path") -local api = a.api -local fn = a.fn --- @brief Neopyter provide a global `jupyterlab` represent remote `JupyterLab` instance, --- which provides some RPC-based API to control remote `JupyterLab` instance. @@ -21,7 +18,7 @@ local fn = a.fn --- --- ``` --- NOTICE: Most API is need a async context, but neopyter provide a wrapped async context ---- automatically +--- automatically. Check `neopyter-async-api` for more details. ---@class neopyter.JupyterOption ---@field auto_activate_file? boolean @@ -68,23 +65,23 @@ end ---attach autocmd function JupyterLab:attach() local config = require("neopyter").config - self.augroup = api.nvim_create_augroup("neopyter-jupyterlab", { clear = true }) + self.augroup = a.api.nvim_create_augroup("neopyter-jupyterlab", { clear = true }) assert(self.augroup ~= nil, "autogroup failed") - api.nvim_create_autocmd({ "BufWinEnter" }, { + a.api.nvim_create_autocmd({ "BufWinEnter" }, { group = self.augroup, pattern = config.file_pattern, callback = function(event) self:_on_bufwinenter(event.buf) end, }) - api.nvim_create_autocmd({ "BufUnload" }, { + a.api.nvim_create_autocmd({ "BufUnload" }, { group = self.augroup, pattern = config.file_pattern, callback = function(event) self:_on_buf_unloaded(event.buf) end, }) - api.nvim_exec_autocmds("BufWinEnter", { + a.api.nvim_exec_autocmds("BufWinEnter", { group = self.augroup, pattern = config.file_pattern, }) @@ -119,8 +116,8 @@ function JupyterLab:connect(address) utils.notify_warn( string.format( "Neovim plugin(neopyter==%s) but Jupyterlab extension(neopyter==%s)\n" - .. "The version do not match!\n" - .. "Please update your neopyter of JupyterLab via `pip install -U neopyter`", + .. "The version do not match!\n" + .. "Please update your neopyter of JupyterLab via `pip install -U neopyter`", nvim_version, jupyterlab_version ) @@ -133,10 +130,10 @@ function JupyterLab:connect(address) end end self.client:connect(address, function() - a.run(on_connected, function() end) + a.run(on_connected) end) - api.nvim_exec_autocmds("BufWinEnter", { + a.api.nvim_exec_autocmds("BufWinEnter", { group = self.augroup, pattern = config.file_pattern, }) @@ -157,16 +154,15 @@ end ---@param buf integer ---@return string function JupyterLab:_get_buf_local_path(buf) - ---@type Path local file_path = utils.get_buf_path(buf) - - if file_path:is_absolute() then - file_path = Path:new(file_path:make_relative(fn.getcwd())) + if vim.fn.isabsolutepath(file_path) == 1 then + file_path = vim.fs.relpath(a.fn.getcwd(), file_path) end - return tostring(file_path) + return file_path end ---if not exists, create with buf +---@async ---@param buf number function JupyterLab:_on_bufwinenter(buf) local jupyter = require("neopyter.jupyter") @@ -210,23 +206,25 @@ function JupyterLab:_on_buf_unloaded(buf) end ---get neopyter (jupyterlab extension) version +---@async ---@return string function JupyterLab:get_jupyterlab_extension_version() return self.client:request("getVersion") end ---get neopyter (nvim plugin) version +---@async ---@return string function JupyterLab:get_nvim_plugin_version() - ---@type Path - local path = utils.get_plugin_path() / "package.json" - local content = path:read() + local path = vim.fs.joinpath(utils.get_plugin_path(), "package.json") + local content = a.fn.readblob(path) ---@cast content -nil local packageJson = vim.json.decode(content) return packageJson["version"] end ---test connection will return `hello: {msg}` as response +---@async ---@param msg string ---@return string|nil function JupyterLab:echo(msg) @@ -234,22 +232,41 @@ function JupyterLab:echo(msg) end ---execute jupyter lab's commands +---[View documents](https://jupyterlab.readthedocs.io/en/stable/user/commands.html#commands-list) +--- +---Example: +---```lua +---require("neopyter.jupyter").jupyterlab:execute_cmd("notebook:export-to-format", { format = "html" }) +---``` +---@async +---@param cmd string +---@param args? table +---@return any +function JupyterLab:execute_cmd(cmd, args) + return self.client:request("executeCommand", cmd, args) +end + +---execute jupyter lab's commands +---@async +---@deprecated use execute_cmd instead +---@nodoc ---@param command string ---@param args? table ---@return any ----[View documents](https://jupyterlab.readthedocs.io/en/stable/user/commands.html#commands-list) function JupyterLab:execute_command(command, args) - return self.client:request("executeCommand", command, args) + return self:execute_cmd(command, args) end ---create new notebook, and selected it ---@nodoc ---@deprecated +---@async function JupyterLab:create_new(ipynb_path, widget_name, kernel) return self.client:request("createNew", ipynb_path, widget_name, kernel) end ---get current notebook of jupyter lab +---@async ---@return string function JupyterLab:current_ipynb() return self.client:request("getCurrentNotebook") diff --git a/lua/neopyter/jupyter/notebook.lua b/lua/neopyter/jupyter/notebook.lua index 692c747..5c00c92 100644 --- a/lua/neopyter/jupyter/notebook.lua +++ b/lua/neopyter/jupyter/notebook.lua @@ -1,6 +1,5 @@ local utils = require("neopyter.utils") local a = require("neopyter.async") -local api = a.api --- @brief Neopyter provide `neopyter.Notebook` represent remote `Notebook` instance, --- which provides some RPC-based API to control remote `Notebook` instance. @@ -42,12 +41,14 @@ function Notebook:new(o) end ---attach autocmd. While connecting with jupyterlab, will full sync +---which means parsing whole notebook and there is a remote associated notebook in jupyterlab +---@async function Notebook:attach() local config = require("neopyter").config if self.augroup == nil then - self.augroup = api.nvim_create_augroup(string.format("neopyter-notebook-%d", self.bufnr), { clear = true }) + self.augroup = a.api.nvim_create_augroup(string.format("neopyter-notebook-%d", self.bufnr), { clear = true }) if config.jupyter.scroll.enable then - api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { + a.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { buffer = self.bufnr, callback = function() if not self:safe_sync() then @@ -65,7 +66,7 @@ function Notebook:attach() }) end - api.nvim_create_autocmd({ "BufWritePre" }, { + a.api.nvim_create_autocmd({ "BufWritePre" }, { buffer = self.bufnr, callback = function() if self:safe_sync() then @@ -74,7 +75,7 @@ function Notebook:attach() end, group = self.augroup, }) - api.nvim_buf_attach(self.bufnr, false, { + a.api.nvim_buf_attach(self.bufnr, false, { on_lines = function(_, _, _, start_row, old_end_row, new_end_row, _) vim.schedule(function() a.run(function() @@ -108,7 +109,7 @@ end --- detach autocmd function Notebook:detach() - api.nvim_del_augroup_by_id(self.augroup) + a.api.nvim_del_augroup_by_id(self.augroup) -- detach buf self.augroup = nil end @@ -119,6 +120,7 @@ function Notebook:is_attached() return self.augroup ~= nil end +---@async function Notebook:is_connecting() if self.client:is_connecting() then if self._is_exist ~= nil then @@ -149,13 +151,14 @@ function Notebook:get_parser() end function Notebook:parse() - local lines = api.nvim_buf_get_lines(self.bufnr, 0, -1, true) + local lines = a.api.nvim_buf_get_lines(self.bufnr, 0, -1, true) local inotebook = self:get_parser():parse_notebook(lines) self.metadata = inotebook.metadata self.cells = inotebook.cells end ---internal request +---@async ---@param method string ---@param ... any ---@return any @@ -165,6 +168,7 @@ function Notebook:_request(method, ...) end ---is exist corresponding notebook in remote server +---@async function Notebook:is_exist() local val = self:_request("isFileExist") self._is_exist = val @@ -172,15 +176,18 @@ function Notebook:is_exist() end --- whether corresponding `.ipynb` file opened in jupyter lab or not +---@async ---@return boolean function Notebook:is_open() return self:_request("isFileOpen") end +---@async function Notebook:open() return self:_request("openFile") end +---@async function Notebook:open_or_reveal() return self:_request("openOrReveal") end @@ -192,11 +199,13 @@ function Notebook:activate() end ---active cell +---@async function Notebook:activate_cell(idx) return self:_request("activateCell", idx) end ---scroll to item +---@async ---@param idx number ---@param align? neopyter.ScrollToAlign ---@param margin? number @@ -205,6 +214,7 @@ function Notebook:scroll_to_item(idx, align, margin) return self:_request("scrollToItem", idx, align, margin) end +---@async function Notebook:get_cell_num() return self:_request("getCellNum") end @@ -273,6 +283,7 @@ function Notebook:get_cell_source(index) return nil end +---@async function Notebook:full_sync() local cells = vim.iter(self.cells) :map(function(cell) @@ -287,6 +298,7 @@ function Notebook:full_sync() end ---partial sync +---@async ---@param start_row integer ---@param old_end_row integer ---@param new_end_row integer @@ -334,42 +346,51 @@ function Notebook:partial_sync(start_row, old_end_row, new_end_row) end ---save ipynb, same as `Ctrl+S` on jupyter lab +---@async ---@return boolean function Notebook:save() return self:_request("save") end +---@async function Notebook:run_selected_cell() return self:_request("runSelectedCell") end +---@async function Notebook:run_all_above() return self:_request("runAllAbove") end +---@async function Notebook:run_all_below() return self:_request("runAllBelow") end +---@async function Notebook:run_all() return self:_request("runAll") end +---@async function Notebook:restart_kernel() return self:_request("restartKernel") end +---@async function Notebook:restart_run_all() return self:_request("restartRunAll") end ---set notebook mode +---@async ---@param mode "command"|"edit" function Notebook:set_mode(mode) return self:_request("setMode", mode) end ---code completion +---@async ---@param options neopyter.CompletionParams ---@return neopyter.CompletionItem[] function Notebook:complete(options) @@ -378,6 +399,7 @@ function Notebook:complete(options) end ---code completion, but kernel complete only +---@async ---@param source string ---@param offset number ---@return {label: string, type: string, insertText:string, source: string}[] @@ -385,6 +407,7 @@ function Notebook:kernel_complete(source, offset) return self:_request("kernelComplete", source, offset) end +---@async function Notebook:run_cell_and_insert_below() self:run_selected_cell() local idx = self:get_cursor_cell_pos() @@ -399,6 +422,7 @@ end ---run selected cell and select next ---fallback to run_cell_and_insert_below if selecting last cell (the behavior is same with `notebook:run-cell-and-select-next`) +---@async function Notebook:run_cell_and_select_next() local idx = self:get_cursor_cell_pos() if idx == #self.cells then diff --git a/lua/neopyter/rpc/baseclient.lua b/lua/neopyter/rpc/baseclient.lua index 368f77c..974e98d 100644 --- a/lua/neopyter/rpc/baseclient.lua +++ b/lua/neopyter/rpc/baseclient.lua @@ -19,7 +19,6 @@ end ---start connect ---@param address? string ---@param on_connected? fun() # call while connected ----@async function RpcClient:connect(address, on_connected) assert(false, "not implement") end diff --git a/lua/neopyter/rpc/direct.lua b/lua/neopyter/rpc/direct.lua index 75ac44c..54e1b60 100644 --- a/lua/neopyter/rpc/direct.lua +++ b/lua/neopyter/rpc/direct.lua @@ -3,7 +3,7 @@ local RpcClient = require("neopyter.rpc.baseclient") local msgpack = require("neopyter.rpc.msgpack") local logger = require("neopyter.logger") local websocket = require("websocket") -local a = require("plenary.async") +local a = require("neopyter.async") ---@class neopyter.DirectRpcClient:neopyter.RpcClient ---@field server? websocket.Server # nil means not connect diff --git a/lua/neopyter/rpc/proxy.lua b/lua/neopyter/rpc/proxy.lua index bf7b458..8a8d0bc 100644 --- a/lua/neopyter/rpc/proxy.lua +++ b/lua/neopyter/rpc/proxy.lua @@ -2,7 +2,7 @@ local utils = require("neopyter.utils") local RpcClient = require("neopyter.rpc.baseclient") local msgpack = require("neopyter.rpc.msgpack") local logger = require("neopyter.logger") -local a = require("plenary.async") +local a = require("neopyter.async") ---@class neopyter.AsyncRpcClient:neopyter.RpcClient ---@field tcp_client? uv_tcp_t # nil means not connect @@ -33,7 +33,7 @@ function ProxyRpcClient:connect(address, on_connected) self.address = address or self.address assert(self.tcp_client == nil, "current connection exists, can't call connect, please disconnect first") assert(self.address, "Rpc client address is empty") - self.tcp_client = vim.loop.new_tcp() + self.tcp_client = vim.uv.new_tcp() local host, port = utils.parse_address(self.address) local err = a.uv.tcp_connect(self.tcp_client, host, port) if err ~= nil then @@ -82,6 +82,7 @@ function ProxyRpcClient:gen_id() end ---send request to server +---@async ---@param method string ---@param ... unknown # name ---@return unknown|nil diff --git a/lua/neopyter/treesitter.lua b/lua/neopyter/treesitter.lua index e135001..c15c5cc 100644 --- a/lua/neopyter/treesitter.lua +++ b/lua/neopyter/treesitter.lua @@ -1,9 +1,4 @@ -local Path = require("plenary.path") local utils = require("neopyter.utils") -local a = require("neopyter.async") -local uv = a.uv -local fn = a.fn -local api = a.api --- @brief Neopyter don't depend on `nvim-treesitter`, this module provides some utility related to `treesitter`, such as capture and match. --- @@ -29,30 +24,35 @@ end function treesitter.load_query(query, lang, name) lang = lang or "python" - ---@type Path - local cache_path = Path:new(a.fn.tempname()) / "neopyter" + local cache_path = vim.fs.joinpath(vim.fn.tempname(), "neopyter") if name then - cache_path = cache_path / name + cache_path = vim.fs.joinpath(cache_path, name) end - ---@type Path - local target_path = cache_path / "queries" / lang / (query .. ".scm") + local target_path = vim.fs.joinpath(cache_path, "queries", lang, query .. ".scm") - ---@type Path local plugin_path = utils.get_plugin_path() - ---@type Path local source_path if name then - source_path = plugin_path / "res" / "queries" / lang / query / (name .. ".scm") + source_path = vim.fs.joinpath(plugin_path, "res", "queries", lang, query, name .. ".scm") else - source_path = plugin_path / "res" / "queries" / lang / (query .. ".scm") + source_path = vim.fs.joinpath(plugin_path, "res", "queries", lang, query .. ".scm") end - if not source_path:exists() then + if not vim.uv.fs_stat(source_path) then utils.notify_warn(string.format("The query %s don't exists in %s", query, source_path)) return end - target_path:parent():mkdir({ parents = true, exists_ok = true }) - source_path:copy({ destination = target_path, parents = true }) + local target_dir = vim.fs.dirname(target_path) + if target_dir then + vim.fn.mkdir(target_dir, "p") + end + + local ok, err = vim.uv.fs_copyfile(source_path, target_path) + if not ok then + utils.notify_warn(string.format("Failed to copy query from %s to %s: %s", source_path, target_path, err)) + return + end + vim.opt.rtp:prepend(tostring(cache_path)) end @@ -82,13 +82,13 @@ function treesitter.iter_captures(query, source, capture, start, stop, loop) local iter = query:iter_captures(root, lang_tree:source(), start, stop) return vim.iter(function() - local ret = { iter() } - if #ret < 1 and loop then - iter = query:iter_captures(root, lang_tree:source(), start, stop) - ret = { iter() } - end - return unpack(ret) - end) + local ret = { iter() } + if #ret < 1 and loop then + iter = query:iter_captures(root, lang_tree:source(), start, stop) + ret = { iter() } + end + return unpack(ret) + end) :filter(function(id, node) ---- FIX: https://github.com/neovim/neovim/issues/31963 if stop then @@ -212,4 +212,5 @@ function treesitter.include_whitespace(bufnr, textobject, selection_mode) end return { start_row, start_col, end_row, end_col } end + return treesitter diff --git a/lua/neopyter/utils.lua b/lua/neopyter/utils.lua index 714fb56..708f01d 100644 --- a/lua/neopyter/utils.lua +++ b/lua/neopyter/utils.lua @@ -1,33 +1,32 @@ -local Path = require("plenary.path") local a = require("neopyter.async") -local api = a.api +local api = vim.api local M = {} local source = debug.getinfo(1).source -- local __dirname__ = source:match("@(.*/)") or source:match("@(.*\\)") local __root__ = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":h:h:h") -local is_windows = vim.loop.os_uname().version:match("Windows") +local is_windows = vim.uv.os_uname().version:match("Windows") if is_windows then __root__ = __root__:gsub("/", "\\") end ---get plugin root ----@return Path +---@return string function M.get_plugin_path() - return Path:new(__root__) + return __root__ end ---@param buf number ----@return Path +---@return string function M.get_buf_path(buf) local file_path = api.nvim_buf_get_name(buf) file_path = vim.fs.normalize(file_path) if is_windows then file_path = file_path:gsub("/", "\\") end - return Path:new(file_path) + return file_path end --- @@ -75,15 +74,6 @@ function M.notify_error(msg) end) end ----get relative path ----@param parent_path string ----@param file_path string ----@return string -function M.relative_to(file_path, parent_path) - local path = Path:new(file_path) - return path:make_relative(parent_path) -end - function M.buf2winid(bufnr) for _, win in ipairs(api.nvim_list_wins()) do if api.nvim_win_get_buf(win) == bufnr then @@ -98,7 +88,7 @@ end ---@return number port function M.parse_address(address) local host, port = address:match("^(.-):(%d+)$") - return host, tonumber(port)--[[@as number]] + return host, tonumber(port) --[[@as number]] end -- ======= Code mostly based on Saghen/blink.cmp === @@ -132,6 +122,7 @@ function M.validate_config(path, tbl, source) end end +---@async ---@generic T ---@param fn T callable ---@param timeout integer? # milliseconds @@ -148,7 +139,7 @@ function M.throttle(fn, timeout, ...) timer:start(timeout, 0, function() a.run(function() fn(unpack(args)) - end, function() end) + end) end) end end diff --git a/plugin/neopyter.lua b/plugin/neopyter.lua index f25e90f..353b26e 100644 --- a/plugin/neopyter.lua +++ b/plugin/neopyter.lua @@ -1,6 +1,6 @@ local jupyter = require("neopyter.jupyter") local utils = require("neopyter.utils") -local a = require("plenary.async") +local a = require("neopyter.async") local cmds = { connect = { @@ -109,7 +109,7 @@ vim.api.nvim_create_user_command("Neopyter", function(opts) table.remove(opts.fargs, 1) a.run(function() cmd.execute(unpack(opts.fargs)) - end, function() end) + end) end end, { desc = "Neopyter manager", diff --git a/scripts/gen_doc.lua b/scripts/gen_doc.lua index 0fc2d0c..f7c9cfb 100644 --- a/scripts/gen_doc.lua +++ b/scripts/gen_doc.lua @@ -9,7 +9,6 @@ vim.opt.packpath:append(vim.fs.joinpath(vim.fn.stdpath("data"), "site")) local __project_root__ = vim.fs.dirname(vim.fs.dirname(vim.fs.abspath(debug.getinfo(1).source:sub(2)))) vim.pack.add({ - "https://github.com/nvim-lua/plenary.nvim", "https://github.com/pysan3/pathlib.nvim", "https://github.com/nvim-neotest/nvim-nio", "https://github.com/kdheepak/panvimdoc", @@ -30,6 +29,7 @@ local Path = require("pathlib") local nvim_gen_files = { "gen_vimdoc.lua", + "lint.lua", "util.lua", "luacats_grammar.lua", "luacats_parser.lua", @@ -78,21 +78,33 @@ local config = { "jupyterlab.lua", "notebook.lua", "treesitter.lua", + "async.lua", }, files = { "lua/neopyter/jupyter/jupyterlab.lua", "lua/neopyter/jupyter/notebook.lua", "lua/neopyter/treesitter.lua", + "lua/neopyter/async.lua", }, fn_xform = function(fun) if fun.module and vim.endswith(fun.module, "treesitter.lua") then return end + if fun.module and vim.endswith(fun.module, "async.lua") then + return + end if vim.startswith(fun.name, "treesitter") then fun.name = vim.split(fun.name, "%.")[2] fun.module = "lua.neopyter.jupyter.treesitter.lua" return end + + if vim.startswith(fun.name, "async") then + fun.name = vim.split(fun.name, "%.")[2] + fun.module = "lua.neopyter.jupyter.async.lua" + return + end + if fun.classvar then return end @@ -109,6 +121,11 @@ local config = { if fun.module and vim.endswith(fun.module, "treesitter.lua") then return "neopyter-treesitter-" .. fun.name end + + if fun.module and vim.endswith(fun.module, "async.lua") then + return "neopyter-async-" .. fun.name + end + local fn_sfx = fun.table and "" or "()" if fun.classvar then return fmt("%s:%s%s", fun.classvar, fun.name, fn_sfx) @@ -157,7 +174,7 @@ local function download_nvim_gen() for i, file in pairs(nvim_gen_files) do local cmd = - string.format("curl -s https://raw.githubusercontent.com/neovim/neovim/refs/heads/master/src/gen/%s -o %s", file, script_path / file) + string.format("curl -s https://raw.githubusercontent.com/neovim/neovim/refs/heads/release-0.12/src/gen/%s -o %s", file, script_path / file) vim.fn.system(cmd) end local entry = entry_script:fs_read()