From 14af275c5cd3fb2a7f07e0143b96b8b83fe760e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20H=C3=A4nel?= Date: Wed, 2 Oct 2024 14:54:32 +0200 Subject: [PATCH] Initial commit --- .stylua.toml | 7 + lua/cave/config.lua | 75 ++++++++ lua/cave/context.lua | 30 +++ lua/cave/enum.lua | 23 +++ lua/cave/env.lua | 62 ++++++ lua/cave/init.lua | 51 +++++ lua/cave/log/class_logger.lua | 46 +++++ lua/cave/log/function_logger.lua | 65 +++++++ lua/cave/log/init.lua | 25 +++ lua/cave/log/logger.lua | 39 ++++ lua/cave/manager.lua | 188 +++++++++++++++++++ lua/cave/meta.lua | 238 ++++++++++++++++++++++++ lua/cave/option.lua | 13 ++ lua/cave/path.lua | 92 +++++++++ lua/cave/project.lua | 235 +++++++++++++++++++++++ lua/cave/python/init.lua | 9 + lua/cave/python/interpreter.lua | 236 +++++++++++++++++++++++ lua/cave/python/module.lua | 69 +++++++ lua/cave/python/runnable.lua | 31 +++ lua/cave/python/script.lua | 76 ++++++++ lua/cave/task.lua | 124 ++++++++++++ lua/cave/template.lua | 0 lua/cave/template_provider.lua | 23 +++ lua/cave/util.lua | 84 +++++++++ lua/overseer/component/dap.lua | 30 +++ mod/__pycache__/spawner.cpython-312.pyc | Bin 0 -> 157 bytes mod/spawner.py | 0 27 files changed, 1871 insertions(+) create mode 100644 .stylua.toml create mode 100644 lua/cave/config.lua create mode 100644 lua/cave/context.lua create mode 100644 lua/cave/enum.lua create mode 100644 lua/cave/env.lua create mode 100644 lua/cave/init.lua create mode 100644 lua/cave/log/class_logger.lua create mode 100644 lua/cave/log/function_logger.lua create mode 100644 lua/cave/log/init.lua create mode 100644 lua/cave/log/logger.lua create mode 100644 lua/cave/manager.lua create mode 100644 lua/cave/meta.lua create mode 100644 lua/cave/option.lua create mode 100644 lua/cave/path.lua create mode 100644 lua/cave/project.lua create mode 100644 lua/cave/python/init.lua create mode 100644 lua/cave/python/interpreter.lua create mode 100644 lua/cave/python/module.lua create mode 100644 lua/cave/python/runnable.lua create mode 100644 lua/cave/python/script.lua create mode 100644 lua/cave/task.lua create mode 100644 lua/cave/template.lua create mode 100644 lua/cave/template_provider.lua create mode 100644 lua/cave/util.lua create mode 100644 lua/overseer/component/dap.lua create mode 100644 mod/__pycache__/spawner.cpython-312.pyc create mode 100644 mod/spawner.py diff --git a/.stylua.toml b/.stylua.toml new file mode 100644 index 0000000..dc9e64f --- /dev/null +++ b/.stylua.toml @@ -0,0 +1,7 @@ +column_width = 110 +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 2 +quote_style = "AutoPreferDouble" +call_parentheses = "None" +collapse_simple_statement = "Always" diff --git a/lua/cave/config.lua b/lua/cave/config.lua new file mode 100644 index 0000000..dbf61d1 --- /dev/null +++ b/lua/cave/config.lua @@ -0,0 +1,75 @@ +local Meta = require "cave.meta" +local Python = require "cave.python" +local Context = require "cave.context" + +local Map = Meta.Map +local Optional = Meta.Optional +local Str = Meta.String +local validate = Meta.validate +local Table = Meta.Table + +---@class cave.Config +---@field python_interpreters table +local Config = Meta.derive "Config" + +---@return overseer.TemplateDefinition[] +function Config:templates() + local templates = {} + for _, python_interpreter in pairs(self.python_interpreters) do + vim.list_extend(templates, python_interpreter:templates()) + end + return templates +end + +---@class cave.Config.Factory : cave.Context +---@field python_interpreters_ cave.Python.Interpreter.Factory[] +---@field context_ cave.Context +local Factory = Meta.derive("Config.Factory", Context) + +function Factory:init(context) + Context.init(self, context.dir, context.name, context.uuid) + self.python_interpreters_ = {} + self.context_ = context +end + +---@param context cave.Context +---@return cave.Config.Factory +function Factory.new(context) + validate { context = { context, Context } } + local factory = setmetatable({}, Factory) + factory:init(context) + return factory +end + +---@return cave.Python.Interpreter.Factory[] +function Factory:get_python_interpreters() return self.python_interpreters_ end + +---@return cave.Python.Interpreter.Factory +function Factory:python() + local python_interpreter = Python.Interpreter.Factory.new(self.context_) + table.insert(self.python_interpreters_, python_interpreter) + return python_interpreter +end + +---@param config cave.Config +function Factory:init_config(config) + config.python_interpreters = {} + for _, python_interpreter_factory in pairs(self:get_python_interpreters()) do + local python_interpreter = python_interpreter_factory:build() + assert(config.python_interpreters[python_interpreter.id] == nil) + config.python_interpreters[python_interpreter.id] = python_interpreter + end +end + +---@return cave.Config +function Factory:build() + local config = setmetatable({}, Config) + self:init_config(config) + return config +end + +---@alias cave.Config.Builder fun(config: cave.Config.Factory) + +Config.Factory = Factory + +return Config diff --git a/lua/cave/context.lua b/lua/cave/context.lua new file mode 100644 index 0000000..9305259 --- /dev/null +++ b/lua/cave/context.lua @@ -0,0 +1,30 @@ +local Meta = require "cave.meta" +local Path = require "cave.path" + +local Str = Meta.String +local validate = Meta.validate + +---@class cave.Context +---@field dir cave.Path +---@field name string +---@field uuid string +local Context = Meta.derive "Context" + +---@param dir cave.Path +---@param name string +---@param uuid string +function Context:init(dir, name, uuid) + validate { dir = { dir, Path }, uuid = { uuid, Str }, name = { name, Str } } + self.dir = dir + self.name = name + self.uuid = uuid +end + +---@return string +function Context:tostring() + return ("Context(dir=%q, name=%q, uuid=%q)"):format(self.dir, self.name, self.uuid) +end + +Context.__tostring = Context.tostring + +return Context diff --git a/lua/cave/enum.lua b/lua/cave/enum.lua new file mode 100644 index 0000000..1888720 --- /dev/null +++ b/lua/cave/enum.lua @@ -0,0 +1,23 @@ +local Meta = require "cave.meta" +local validate = Meta.validate +local Map = Meta.Map +local Str = Meta.String + +---@class cave.Enum +local Enum = {} +Enum.__index = Enum + +---@param tbl table +---@param name string +function Enum.new(tbl, name) + validate { + tbl = { tbl, Map(Str, Str) }, + name = { name, Str }, + } + for key, value in pairs(tbl) do + assert(key == value) + end + setmetatable(tbl, { __meta = Meta.Enum(tbl, name) }) +end + +return Enum diff --git a/lua/cave/env.lua b/lua/cave/env.lua new file mode 100644 index 0000000..1b0a55d --- /dev/null +++ b/lua/cave/env.lua @@ -0,0 +1,62 @@ +local Meta = require "cave.meta" +local Util = require "cave.util" +local Context = require "cave.context" +local ClassLogger = require "cave.log.class_logger" + +local validate = Meta.validate +local Optional = Meta.Optional + +---@class cave.Env +---@field vars table +---@field on_update_cbs function[] +local Env = Meta.derive "Env" +Env.log = ClassLogger.new(Env) + +---@param context cave.Context? +---@return cave.Env +function Env:new(context) + local env = setmetatable({}, Env) + env.vars = {} + local dir, name, uuid + if context then + dir, name, uuid = tostring(context.dir), context.name, context.uuid + else + dir, name, uuid = nil, nil, nil + end + vim.fn.setenv("NVIM_PROJECT_DIR", dir) + vim.fn.setenv("NVIM_PROJECT_NAME", name) + vim.fn.setenv("NVIM_PROJECT_UUID", uuid) + for _, entry in ipairs(vim.fn.systemlist { "nvim_env" }) do + local split_idx = entry:find "=" + assert(split_idx ~= nil) + env.vars[entry:sub(1, split_idx - 1)] = entry:sub(split_idx + 1, -1) + end + return env +end + +---@param context cave.Context? +function Env:update(context) + validate { context = { context, Optional(Context) } } + local log = Env.log:call("%s", context) + + local new_env = Env:new(context) + local env_var_diff = Util.tbl_diff(self.vars, new_env.vars) + + for name, value in pairs(env_var_diff.added) do + vim.fn.setenv(name, value) + end + for name, values in pairs(env_var_diff.modified) do + vim.fn.setenv(name, values.new) + end + for name, _ in pairs(env_var_diff.removed) do + vim.fn.setenv(name, nil) + end + + self.vars = new_env.vars + + -- vim.g.python3_host_prog = get_python3_host_prog() + + log:ok("environment variables changed:\n%s", vim.inspect(env_var_diff)) +end + +return Env diff --git a/lua/cave/init.lua b/lua/cave/init.lua new file mode 100644 index 0000000..cbdf936 --- /dev/null +++ b/lua/cave/init.lua @@ -0,0 +1,51 @@ +---@class cave +local cave = {} +cave.Path = require "cave.path" +cave.Util = require "cave.util" + +cave.manager = require("cave.manager").new() + +function cave.load_project_from_cwd() + local manager = cave.manager + local workspaces = require "workspaces" + local Path = cave.Path + local cwd = Path.cwd() + for project_name, project in pairs(manager:get_projects()) do + if project.dir:samefile(cwd) then workspaces.open(project_name) end + end +end + +function cave.setup() + vim.api.nvim_create_user_command( + "ProjectAdd", + function(cmd_opts) cave.manager:add_project(unpack(cmd_opts.fargs)) end, + { + desc = "Add a project (dir?, name?)", + nargs = "*", + complete = "file", + } + ) + + vim.api.nvim_create_user_command( + "ProjectRemove", + function(cmd_opts) cave.manager:remove_project(unpack(cmd_opts.fargs)) end, + { + desc = "Remove a project (name?)", + nargs = 1, + complete = function(lead) return cave.manager:project_name_complete(lead) end, + } + ) + vim.api.nvim_create_user_command( + "ProjectRename", + function(cmd_opts) cave.manager:rename_project(unpack(cmd_opts.fargs)) end, + { + desc = "Rename a project. (old_name, new_name)", + nargs = "+", + complete = function(lead) return cave.manager:project_name_complete(lead) end, + } + ) + + vim.schedule(cave.load_project_from_cwd) +end + +return cave diff --git a/lua/cave/log/class_logger.lua b/lua/cave/log/class_logger.lua new file mode 100644 index 0000000..0ad5d91 --- /dev/null +++ b/lua/cave/log/class_logger.lua @@ -0,0 +1,46 @@ +local Logger = require "cave.log.logger" +local FunctionLogger = require "cave.log.function_logger" +local Meta = require "cave.meta" + +local validate = Meta.validate +local Table = Meta.Table + +---@class cave.ClassLogger : cave.Logger +local ClassLogger = Meta.derive("ClassLogger", Logger) + +---@param cls_name string +function ClassLogger:init(cls_name) + local prefix = cls_name + Logger.init(self, prefix) +end + +---@param cls_mt table +---@return cave.ClassLogger +function ClassLogger.new(cls_mt) + validate { cls_mt = { cls_mt, Table } } + local cls_name = Meta.like(cls_mt).repr + local logger = setmetatable({}, ClassLogger) + logger:init(cls_name) + return logger +end + +---@param args_fmt string? +---@param ... any +---@return cave.FunctionLogger +function ClassLogger:call(args_fmt, ...) + local dbg_info + local fn_name + for l = 2, 1, -1 do + dbg_info = debug.getinfo(l, "n") + fn_name = dbg_info.name + if fn_name then break end + end + if dbg_info.namewhat == "method" then + fn_name = self.prefix .. ":" .. fn_name + elseif dbg_info.namewhat == "field" then + fn_name = self.prefix .. "." .. fn_name + end + return FunctionLogger.new(fn_name, args_fmt, ...):call() +end + +return ClassLogger diff --git a/lua/cave/log/function_logger.lua b/lua/cave/log/function_logger.lua new file mode 100644 index 0000000..a41bdf9 --- /dev/null +++ b/lua/cave/log/function_logger.lua @@ -0,0 +1,65 @@ +local Log = require "cave.log" +local Meta = require "cave.meta" + +local Str = Meta.String +local validate = Meta.validate +local Optional = Meta.Optional + +---@class cave.FunctionLogger +---@field fn_repr string +local FunctionLogger = Meta.derive "FuncLogger" + +---@param fn_name string +---@param args_fmt string? +---@param ... any +function FunctionLogger:init(fn_name, args_fmt, ...) + local args_repr = (args_fmt and args_fmt:format(...)) or "" + self.fn_repr = ("%s(%s)"):format(fn_name, args_repr) +end + +---@param fn_name string +---@param args_fmt string? +---@param ... any +---@return cave.FunctionLogger +function FunctionLogger.new(fn_name, args_fmt, ...) + validate { fn_name = { fn_name, Str }, args_fmt = { args_fmt, Optional(Str) } } + local logger = setmetatable({}, FunctionLogger) + logger:init(fn_name, args_fmt, ...) + return logger +end + +---@return cave.FunctionLogger +function FunctionLogger:call() + Log.dbg("> %s", self.fn_repr) + return self +end + +---@param err_fmt string? +---@param ... any +function FunctionLogger:err(err_fmt, ...) + validate { err = { err_fmt, Optional(Str) } } + local res_msg = "err" + if err_fmt ~= nil then res_msg = res_msg .. " " .. err_fmt:format(...) end + Log.err(("< %s - %s"):format(self.fn_repr, res_msg)) +end + +-- +---@param warn_fmt string? +---@param ... any +function FunctionLogger:warn(warn_fmt, ...) + validate { err = { warn_fmt, Optional(Str) } } + local res_msg = "warn" + if warn_fmt ~= nil then res_msg = res_msg .. " " .. warn_fmt:format(...) end + Log.err(("~ %s - %s"):format(self.fn_repr, res_msg)) +end + +---@param res_fmt string? +---@param ... any +function FunctionLogger:ok(res_fmt, ...) + validate { results = { res_fmt, Optional(Str) } } + local res_msg = "ok" + if res_fmt ~= nil then res_msg = res_msg .. " " .. res_fmt:format(...) end + Log.dbg(("< %s - %s"):format(self.fn_repr, res_msg)) +end + +return FunctionLogger diff --git a/lua/cave/log/init.lua b/lua/cave/log/init.lua new file mode 100644 index 0000000..e309e16 --- /dev/null +++ b/lua/cave/log/init.lua @@ -0,0 +1,25 @@ +---@class cave.Log +local Log = {} + +---@param msg string +---@param level integer +---@param ...? any +local function notify(msg, level, ...) vim.notify(msg:format(...), level, { title = "cave.nvim" }) end + +---@param msg string +---@param ...? any +function Log.dbg(msg, ...) notify(msg, vim.log.levels.DEBUG, ...) end + +---@param msg string +---@param ...? any +function Log.err(msg, ...) notify(msg, vim.log.levels.ERROR, ...) end + +---@param msg string +---@param ...? any +function Log.inf(msg, ...) notify(msg, vim.log.levels.INFO, ...) end + +---@param msg string +---@param ...? any +function Log.warn(msg, ...) notify(msg, vim.log.levels.WARN, ...) end + +return Log diff --git a/lua/cave/log/logger.lua b/lua/cave/log/logger.lua new file mode 100644 index 0000000..66a885f --- /dev/null +++ b/lua/cave/log/logger.lua @@ -0,0 +1,39 @@ +local Log = require "cave.log" +local Meta = require "cave.meta" + +local validate = Meta.validate +local Str = Meta.String + +---@class cave.Logger +---@field prefix string +local Logger = Meta.derive "Logger" + +---@param prefix string +function Logger:init(prefix) self.prefix = prefix end + +---@param prefix string +---@return cave.Logger +function Logger.new(prefix) + validate { prefix = { prefix, Str } } + local logger = setmetatable({}, Logger) + logger:init(prefix) + return logger +end + +---@param msg string +---@param ...? any +function Logger:dbg(msg, ...) Log.dbg(self.prefix .. msg, ...) end + +---@param msg string +---@param ...? any +function Logger:err(msg, ...) Log.err(self.prefix .. msg, ...) end + +---@param msg string +---@param ...? any +function Logger:inf(msg, ...) Log.inf(self.prefix .. msg, ...) end + +---@param msg string +---@param ...? any +function Logger:warn(msg, ...) Log.warn(self.prefix .. msg, ...) end + +return Logger diff --git a/lua/cave/manager.lua b/lua/cave/manager.lua new file mode 100644 index 0000000..8f142a9 --- /dev/null +++ b/lua/cave/manager.lua @@ -0,0 +1,188 @@ +local Env = require "cave.env" +local Meta = require "cave.meta" +local Path = require "cave.path" +local Project = require "cave.project" +local Util = require "cave.util" +local ClassLogger = require "cave.log.class_logger" +local TemplateProvider = require "cave.template_provider" + +local workspaces = require "workspaces" +local overseer = require "overseer" + +local Str = Meta.String +local validate = Meta.validate +local Optional = Meta.Optional + +---@class cave.Manager +---@field project? cave.Project +---@field env cave.Env +---@field template_provider cave.TemplateProvider +local Manager = Meta.derive "Manager" +Manager.log = ClassLogger.new(Manager) + +function Manager:init() + self.env = Env:new() + ---@return overseer.TemplateDefinition[] + local function project_templates() return self.project and self.project.templates or {} end + self.template_provider = TemplateProvider.new("cave.nvim", project_templates) + overseer.register_template(self.template_provider) + ---@param task_defn overseer.TaskDefinition + ---@param _ overseer.TaskUtil + local function on_template_run(task_defn, _) + local name = task_defn.name + if name == nil or self.project == nil then return end + self.project:on_template_run(name) + end + + overseer.add_template_hook(nil, on_template_run) +end + +---@return cave.Manager +function Manager.new() + local manager = setmetatable({}, Manager) + manager:init() + return manager +end + +---@return table +function Manager:get_projects() + local projects = {} + if self.project ~= nil then projects[self.project.name] = self.project end + for _, ws in pairs(workspaces.get()) do + local project = Project.new(Path.new(ws.path), ws.name, ws.custom) + if projects[project.name] == nil then projects[project.name] = project end + end + return projects +end + +---@param lead string +---@return string[] +function Manager:project_name_complete(lead) + validate { lead = { lead, Str } } + local projects = self:get_projects() + local project_names = {} + for project_name, _ in pairs(projects) do + if vim.startswith(project_name, lead) then table.insert(project_names, project_name) end + end + return project_names +end + +---@param lead string +---@return string[] +function Manager:project_dir_complete(lead) + validate { lead = { lead, Str } } + local projects = self:get_projects() + local project_dirs = {} + for _, project in pairs(projects) do + local project_dir = tostring(project.dir) + if vim.startswith(project_dir, lead) then table.insert(project_dirs, project_dir) end + end + return project_dirs +end + +---@param dir string? +---@param name string? +function Manager:add_project(dir, name) + validate { dir = { dir, Optional(Str) }, name = { name, Optional(Str) } } + local log = Manager.log:call("dir=%q, name=%s", dir, name and ("%q"):format(name) or "nil") + + local project_dir = Path.new(dir or Path.cwd()) + assert(project_dir:is_absolute()) + local project_name = name or project_dir:basename() + local project_uuid = Util.generate_uuid() + if not project_dir:is_dir() then return log:err("%q is not a directory", project_dir) end + + local projects = self:get_projects() + local project_exists = projects[project_name] ~= nil + if project_exists then return log:err("project with name %q already exists", project_name) end + + local project = Project.new(project_dir, project_name, project_uuid) + + workspaces.add(tostring(project.dir), project.name) + workspaces.set_custom(project.name, project.uuid) + + log:ok() +end + +---@param name string +function Manager:remove_project(name) + validate { name = { name, Str } } + local log = Manager.log:call("name=%q", name) + + local projects = self:get_projects() + local project = projects[name] + if project == nil then return log:err "projectdoesn't exist" end + + if project == self.project then self:close_project() end + + workspaces.remove(name) + + log:ok() +end + +---@param old_name string +---@param new_name string +function Manager:rename_project(old_name, new_name) + validate { new_name = { new_name, Str }, old_name = { old_name, Str } } + local log = Manager.log:call("old_name=%q, new_name=%q", old_name, new_name) + + if new_name == old_name then return log:ok() end + + local projects = self:get_projects() + local project = projects[old_name] + if project == nil then return log:err "project with old name doesn't exist" end + if projects[new_name] ~= nil then return log:err "project with new name already exists" end + + project:rename(new_name) + + if project == self.project then self.env:update() end + + workspaces.rename(old_name, new_name) + + log:ok() +end + +---@param name string +function Manager:open_project(name) + validate { name = { name, Str } } + local log = Manager.log:call("name=%q", name) + + if self.project ~= nil and self.project.name == name then return log:ok() end + + local projects = self:get_projects() + local project = projects[name] + if project == nil then return log:err "project doesn't exist" end + + self:close_project() + self.project = project + + self.env:update(self.project) + + project:open() + + log:ok() +end + +function Manager:open_project_config() + local log = Manager.log:call() + + if self.project == nil then return log:ok() end + + vim.cmd.edit(tostring(self.project.config_script)) + + log:ok() +end + +function Manager:close_project() + local log = Manager.log:call() + if self.project == nil then return log:ok() end + + self.project:close() + self.project = nil + + self.env:update() + + log:ok() +end + +return Manager diff --git a/lua/cave/meta.lua b/lua/cave/meta.lua new file mode 100644 index 0000000..902ac17 --- /dev/null +++ b/lua/cave/meta.lua @@ -0,0 +1,238 @@ +---@alias cave.Validator fun(v: any) : boolean + +---@class cave.Meta +---@field valid cave.Validator +---@field repr string +local Meta = {} +Meta.__index = Meta + +---@alias cave.MetaLikeItem string|table|cave.Meta +---@alias cave.MetaLike cave.MetaLikeItem|cave.MetaLikeItem[] + +---@param repr string +---@param valid cave.Validator +---@return cave.Meta +function Meta.new(repr, valid) + vim.validate { + repr = { repr, "string" }, + valid = { valid, "function" }, + } + local meta = setmetatable({}, Meta) + meta.valid = valid + meta.repr = repr + return meta +end + +---@param ... cave.MetaLikeItem +---@return cave.Meta +function Meta.Union(...) + local value_valids = {} + local value_reprs = {} + for idx, value_meta_like in ipairs { ... } do + local value_meta = Meta.like(value_meta_like) + value_valids[idx] = value_meta.valid + value_reprs[idx] = value_meta.repr + end + local repr = ("Union<%s>"):format(table.concat(value_reprs, ",")) + local function valid(v) + for _, value_valid in pairs(value_valids) do + if value_valid(v) then return true end + end + return false + end + return Meta.new(repr, valid) +end + +---@param value_meta_like cave.MetaLike +---@return cave.Meta +function Meta.Optional(value_meta_like) + local value_meta = Meta.like(value_meta_like) + local repr = ("Optional<%s>"):format(value_meta.repr) + local function valid(v) return v == nil or value_meta.valid(v) end + return Meta.new(repr, valid) +end + +---@param mt table +---@param name string +---@param valid cave.Validator? +---@return cave.Meta +function Meta.Class(mt, name, valid) + assert(type(mt) == "table") + local repr = name + ---@param v any + ---@return boolean + local function default_valid(v) + if type(v) ~= "table" then return false end + local v_mt = getmetatable(v) + while v_mt ~= nil do + if v_mt == mt then return true end + v_mt = getmetatable(v_mt) + end + return false + end + valid = valid or default_valid + return Meta.new(repr, valid) +end + +---@param tbl table +---@param name string +---@return cave.Meta +function Meta.Enum(tbl, name) + assert(type(tbl) == "table") + local repr = ("Enum<%s>(%s)"):format(name, table.concat(vim.tbl_values(tbl), "|")) + local function valid(v) return type(v) == "string" and tbl[v] == v end + return Meta.new(repr, valid) +end + +---@param value_meta_like cave.MetaLike +---@return cave.Meta +function Meta.List(value_meta_like) + local value_meta = Meta.like(value_meta_like) + local repr = ("List<%s>"):format(value_meta.repr) + local function valid(vs) + if not vim.tbl_islist(vs) then return false end + for _, v in ipairs(vs) do + if not value_meta.valid(v) then return false end + end + return true + end + return Meta.new(repr, valid) +end + +---@param key_meta_like cave.MetaLike +---@param value_meta_like cave.MetaLike +---@return cave.Meta +function Meta.Map(key_meta_like, value_meta_like) + local key_meta = Meta.like(key_meta_like) + local value_meta = Meta.like(value_meta_like) + local repr = ("Map<%s,%s>"):format(key_meta.repr, value_meta.repr) + local function valid(vs) + if type(vs) ~= "table" then return false end + for k, v in ipairs(vs) do + if not key_meta.valid(k) or value_meta.valid(v) then return false end + end + return true + end + return Meta.new(repr, valid) +end + +---@param meta_like_tbl table +---@param name string +---@return cave.Meta +function Meta.MapLiteral(meta_like_tbl, name) + ---@type table + local metas = {} + for key, meta_like in meta_like_tbl do + metas[key] = Meta.like(meta_like) + end + ---@param vs any + ---@return boolean + local function valid(vs) + if type(vs) ~= "table" then return false end + for key, meta in pairs(metas) do + local value = vs[key] + if not meta.valid(value) then return false end + end + for key, _ in pairs(vs) do + if metas[key] == nil then return false end + end + return true + end + return Meta.new(name, valid) +end + +Meta.Bool = Meta.new("boolean", function(v) return type(v) == "boolean" end) +Meta.Function = Meta.new("function", function(v) return type(v) == "function" end) +Meta.Number = Meta.new("number", function(v) return type(v) == "number" end) +Meta.String = Meta.new("string", function(v) return type(v) == "string" end) +Meta.Table = Meta.new("table", function(v) return type(v) == "table" end) +Meta.Nil = Meta.new("nil", function(v) return v == nil end) + +local meta_aliases = { + ["b"] = Meta.Bool, + ["bool"] = Meta.Bool, + ["boolean"] = Meta.Bool, + ["f"] = Meta.Function, + ["fn"] = Meta.Function, + ["function"] = Meta.Function, + ["n"] = Meta.Number, + ["num"] = Meta.Number, + ["number"] = Meta.Number, + ["nil"] = Meta.Nil, + ["s"] = Meta.String, + ["str"] = Meta.String, + ["string"] = Meta.String, + ["t"] = Meta.Table, + ["tbl"] = Meta.Table, + ["table"] = Meta.Table, +} + +---@param meta_like cave.MetaLike +---@return cave.Meta +function Meta.like(meta_like) + local meta_like_type = type(meta_like) + local meta + if meta_like_type == "string" then + meta = meta_aliases[meta_like] + else + assert(meta_like_type == "table") + local mt = getmetatable(meta_like) + if mt == Meta then + meta = meta_like + elseif meta_like.__meta ~= nil then + meta = meta_like.__meta + elseif vim.tbl_islist(meta_like) then + meta = Meta.Union(unpack(meta_like)) + else + assert(false) + end + end + assert(getmetatable(meta) == Meta) + return meta +end + +---@return cave.Validator +---@return string +function Meta:check() return self.valid, self.repr end + +---@param params table +function Meta.validate(params) + local vim_params = {} + for param_name, param_specs in pairs(params) do + assert(type(param_name) == "string", "param name is not a string") + assert(type(param_specs) == "table", "param spec for '" .. param_name .. "' is not a table") + assert(#param_specs == 2, "param spec length for '" .. param_name .. "' is not 2") + local param_value = param_specs[1] --[[@as any]] + local param_meta_like = param_specs[2] --[[@as cave.MetaLike]] + local param_meta = Meta.like(param_meta_like) + vim_params[param_name] = { param_value, param_meta:check() } + end + vim.validate(vim_params) +end + +---@param name string +---@param base_mt table? +---@return table +function Meta.derive(name, base_mt) + Meta.validate { name = { name, Meta.String }, base_mt = { base_mt, Meta.Optional(Meta.Table) } } + local mt = {} + mt.__index = mt + mt.__meta = Meta.Class(mt, name) + return (base_mt and setmetatable(mt, base_mt)) or mt +end + +---@param obj table +---@return string +function Meta.get_repr(obj) + Meta.validate { obj = { obj, Meta.Table } } + local mt = getmetatable(obj) + Meta.validate { mt = { mt, Meta.Table } } + local meta = mt.__meta + Meta.validate { meta = { meta, Meta.Table } } + local meta_mt = getmetatable(obj) + assert(meta_mt == Meta) + ---@cast meta cave.Meta + return meta.repr +end + +return Meta diff --git a/lua/cave/option.lua b/lua/cave/option.lua new file mode 100644 index 0000000..9cbec46 --- /dev/null +++ b/lua/cave/option.lua @@ -0,0 +1,13 @@ +---@class cave.Option +local Option + +---@generic ValueType, ResultType +---@param opt ValueType? +---@param fn fun(opt: ValueType): ResultType +---@return ResultType? +function Option.map(opt, fn) + if opt == nil then return nil end + return fn(opt) +end + +return Option diff --git a/lua/cave/path.lua b/lua/cave/path.lua new file mode 100644 index 0000000..8c7fd60 --- /dev/null +++ b/lua/cave/path.lua @@ -0,0 +1,92 @@ +local Meta = require "cave.meta" + +local PosixPath = require "pathlib.posix" --[[@as PathlibPosixPath]] + +local List = Meta.List +local Str = Meta.String +local validate = Meta.validate + +---@alias cave.PathLikeItem string|cave.Path +---@alias cave.PathLike cave.PathLikeItem|cave.PathLikeItem[] + +---@class cave.Path : PathlibPosixPath +---@operator div(cave.PathLikeItem): cave.Path +---@operator concat(cave.PathLikeItem): string +local Path = Meta.derive("Path", PosixPath) +require("pathlib.utils.paths").link_dunders(Path, PosixPath) + +Path.LikeItem = { Str, Path } +Path.LikeItemList = List { Str, Path } +Path.Like = { Str, Path, Path.LikeItemList } + +---@param path_like cave.PathLike +---@return cave.Path +function Path.like(path_like) + validate { path_like = { path_like, Path.Like } } + local self = Path.new_empty() + local path_like_type = type(path_like) + if path_like_type == "string" or not vim.tbl_islist(path_like) then + self:_init(path_like) + else + self:_init(unpack(path_like)) + end + return self +end + +---@param path cave.Path +---@return cave.Path +function Path:copy_all_from(path) + ---@type cave.Path + return PosixPath.copy_all_from(self, path) +end + +---@return cave.Path +function Path:copy() return Path.new_empty():copy_all_from(self) end + +---@param ... cave.PathLikeItem +---@return cave.Path +function Path.new(...) + validate { ["..."] = { { ... }, Path.LikeItemList } } + local self = Path.new_empty() + self:_init(...) + return self +end + +---@return cave.Path +function Path.new_empty() + local self = setmetatable({}, Path) + self:to_empty() + return self +end + +---@return cave.Path +function Path.cwd() return Path.new(vim.fn.getcwd()) end + +---@return cave.Path +function Path.home() return Path.new(vim.loop.os_homedir()) end + +---@param what string +---@param ... cave.PathLikeItem +---@return cave.Path +function Path.stdpath(what, ...) + validate { what = { what, Str }, ["..."] = { { ... }, Path.LikeItemList } } + return Path.new(vim.fn.stdpath(what), ...) +end + +-- ---@return cave.Path +-- function Path:to_absolute(cwd) return PosixPath.to_absolute(self, cwd) end + +---@param name cave.PathLikeItem +---@return cave.Path +function Path.executable(name) + validate { name = { name, { Str, Path } } } + if type(name) ~= "string" then name = tostring(name) end + local s = vim.fn.exepath(name) + assert(#s > 0, ("'%s' is not executable"):format(name)) + return Path.new(s) +end + +---@return boolean +function Path:is_executable() return vim.fn.executable(self:tostring()) == 1 end + +return Path diff --git a/lua/cave/project.lua b/lua/cave/project.lua new file mode 100644 index 0000000..5fc67aa --- /dev/null +++ b/lua/cave/project.lua @@ -0,0 +1,235 @@ +local Path = require "cave.path" +local Util = require "cave.util" +local Meta = require "cave.meta" +local Context = require "cave.context" +local Config = require "cave.config" +local Log = require "cave.log" +local ClassLogger = require "cave.log.class_logger" + +local resession = require "resession" + +local validate = Meta.validate +local Str = Meta.String + +local CONFIG_SCRIPT_TEMPLATE = [[ +---@type cave.Config.Builder +local function configure(config) +end +return configure +]] + +---@class cave.Project : cave.Context +---@field config_dir cave.Path +---@field config_script cave.Path +---@field config_dir_watch number +---@field config cave.Config? +---@field templates overseer.TemplateDefinition[] +---@field template_order table +local Project = Meta.derive("Project", Context) + +Project.log = ClassLogger.new(Project) + +---@param dir cave.Path +---@param name string +---@param uuid string +function Project:init(dir, name, uuid) + Context.init(self, dir, name, uuid) + self.config_dir = Path.stdpath("data", "cave", "project", uuid):to_absolute() --[[@as cave.Path]] + self.config_script = self.config_dir / "config.lua" + self.templates = {} + self.template_order = {} +end + +---@param dir cave.Path +---@param name string +---@param uuid string +---@return cave.Project +function Project.new(dir, name, uuid) + validate { dir = { dir, Path }, name = { name, Str }, uuid = { uuid, Str } } + local project = setmetatable({}, Project) + project:init(dir, name, uuid) + return project +end + +---@return boolean +function Project:session_exists() + for _, session in pairs(resession.list()) do + if session == self.uuid then return true end + end + return false +end + +---@return boolean +function Project:session_active() return resession.get_current() == self.uuid end + +function Project:load_session() + local log = Project.log:call() + if self:session_exists() then + resession.load(self.uuid, { silence_errors = true, notify = false }) + else + Util.close_all_buffers() + self:save_session() + end + + if not self:session_active() then log:warn "project session activation failed" end + log:ok() +end + +function Project:save_session() + local log = Project.log:call() + + resession.save(self.uuid, { attach = true, notify = false }) + + if not self:session_exists() then return log:err "session wasn't created" end + log:ok() +end + +function Project:delete_session() + local log = Project.log:call() + + if not self:session_exists() then return log:ok() end + resession.delete(self.uuid) + + if self:session_exists() then log:err "session wasn't deleted" end + log:ok() +end + +function Project:init_config_dir() + local log = Project.log:call() + + if not self.config_dir:is_dir() and not self.config_dir:mkdir(Path.const.o755, true) then + return log:err("config dir (%q) creation failed\n%s", self.config_dir, self.config_dir.error_msg) + end + + if not self.config_script:exists() and not self.config_script:io_write(CONFIG_SCRIPT_TEMPLATE) then + return log:err("config file (%q) creation failed\n%s", self.config_script, self.config_script.error_msg) + end + + log:ok() +end + +function Project:load_config() + local log = Project.log:call() + + self.config = nil + + if not self.config_script:is_file() then + return log:err("config script (%q) missing", self.config_script) + end + + local chunk, err = loadfile(self.config_script:tostring()) + if chunk == nil then return log:err("config script loading failed\n%s", err) end + + local chunk_ok, chunk_res = pcall(chunk) + if not chunk_ok then return log:err("config script failed\n%s", chunk_res) end + + local res_type = type(chunk_res) + if res_type ~= "function" then return log:err("config script returned %s instead of function", res_type) end + + local config_factory = Config.Factory.new(self) + local configure_ok, configure_res = pcall(chunk_res --[[@as cave.Config.Builder]], config_factory) + if not configure_ok then return log:err("configuring failed\n%s", configure_res) end + + local build_config_ok, build_config_res = pcall(Config.Factory.build, config_factory) + if not build_config_ok then return log:err("building config failed\n%s", configure_res) end + + self.config = build_config_res + log:ok() +end + +function Project:load_templates() + local log = Project.log:call() + self.templates = self.config and self.config:templates() or {} + + local task_order = {} + for _, template in ipairs(self.templates) do + local priority = self.template_order[template.name] + if priority ~= nil then + template.priority = priority + task_order[template.name] = priority + end + end + self.template_order = task_order + log:ok() +end + +---@param name string +function Project:on_template_run(name) + validate { name = { name, Str } } + local log = Project.log:call() + local priority = -os.clock() + for _, template in ipairs(self.templates) do + if template.name == name then + template.priority = priority + self.template_order[name] = priority + break + end + end + log:ok() +end + +function Project:watch_config_dir() + local log = Project.log:call() + + if self.config_dir_watch == nil then + self.config_dir_watch = vim.api.nvim_create_autocmd("BufWritePost", { + callback = vim.schedule_wrap(function(event) + if self.config_dir_watch == nil then return end + if Path.new(event.match):to_absolute():is_relative_to(self.config_dir) then self:reload() end + end), + }) + end + + log:ok() +end + +function Project:unwatch_config_dir() + local log = Project.log:call() + + if self.config_dir_watch then + vim.api.nvim_del_autocmd(self.config_dir_watch) + self.config_dir_watch = nil + end + + log:ok() +end + +function Project:reload() + self:load_config() + self:load_templates() +end + +function Project:open() + local log = Project.log:call() + + self:load_session() + self:init_config_dir() + self:reload() + self:watch_config_dir() + + log:ok() +end + +function Project:close() + local log = Project.log:call() + + self:unwatch_config_dir() + Util.save_all_buffers() + self:save_session() + + log:ok() +end + +---@param new_name string +function Project:rename(new_name) + validate { new_name = { new_name, Str } } + local log = Project.log:call("%q", new_name) + if self.name == new_name then return log:ok() end + + self.name = new_name + if self.config ~= nil then self:reload() end + + log:ok() +end + +return Project diff --git a/lua/cave/python/init.lua b/lua/cave/python/init.lua new file mode 100644 index 0000000..4ea504c --- /dev/null +++ b/lua/cave/python/init.lua @@ -0,0 +1,9 @@ +---@class cave.Python +local Python = { + Interpreter = require "cave.python.interpreter", + Module = require "cave.python.module", + Runnable = require "cave.python.runnable", + Script = require "cave.python.script", +} + +return Python diff --git a/lua/cave/python/interpreter.lua b/lua/cave/python/interpreter.lua new file mode 100644 index 0000000..b100dd3 --- /dev/null +++ b/lua/cave/python/interpreter.lua @@ -0,0 +1,236 @@ +local Context = require "cave.context" +local Meta = require "cave.meta" +local Module = require "cave.python.module" +local Path = require "cave.path" +local Runnable = require "cave.python.runnable" +local Script = require "cave.python.script" +local Task = require "cave.task" + +local List = Meta.List +local Map = Meta.Map +local Str = Meta.String +local validate = Meta.validate + +---@class cave.Python.Interpreter +---@field id string +---@field path cave.Path +---@field args string[] +---@field env table +---@field runnables table +---@field context cave.Context +local Interpreter = Meta.derive "Python.Interpreter" + +---@return string +function Interpreter:valid_path() return self.path:executable():tostring() end + +---@return overseer.TemplateDefinition[] +function Interpreter:templates() + local templates = {} + for _, runnable in pairs(self.runnables) do + vim.list_extend(templates, { self:run_template(runnable), self:debug_template(runnable) }) + end + return templates +end + +---@param runnable cave.Python.Runnable +---@return overseer.TemplateDefinition +function Interpreter:run_template(runnable) + local name = ("python.%s.run.%s"):format(self.id, runnable.id) + local script, module = runnable:concrete() + local args = vim.list_extend({}, self.args) + vim.list_extend( + args, + (script and { script:valid_file() }) or (module and { "-m", module.name }) or error "Unsupported runnable" + ) + vim.list_extend(args, runnable.args) + local template_definition = { + name = name, + builder = function() + ---@type overseer.TaskDefinition + local task_definition = { + name = name, + cmd = { self:valid_path() }, + args = args, + env = vim.tbl_extend("keep", runnable.env, self.env), + cwd = runnable:valid_cwd(), + components = { { "defaults" } }, + } + return task_definition + end, + params = {}, + tags = { Task.Tag.Run }, + } + return template_definition +end + +---@param runnable cave.Python.Runnable +---@return overseer.TemplateDefinition +function Interpreter:debug_template(runnable) + local name = ("python.%s.debug.%s"):format(self.id, runnable.id) + local template_definition = {} + template_definition.builder = function() + local script, module = runnable:concrete() + + ---@type DebugpyLaunchConfig + local dap_config = { + name = name, + type = "python", + request = "launch", + module = module and module.name, + program = script and script:valid_file(), + cwd = runnable:valid_cwd(), + args = runnable.args, + env = vim.tbl_extend("keep", runnable.env, self.env), + python = vim.list_extend({ self:valid_path() }, self.args), + } + + if vim.tbl_isempty(dap_config.env) then dap_config.env = nil end + + ---@type overseer.TaskDefinition + local task_definition = { + name = name, + cmd = {}, + components = { { "defaults" }, { "dap", config = dap_config } }, + } + return task_definition + end + template_definition.params = {} + template_definition.tags = { Task.Tag.Debug } + template_definition.name = name + return template_definition +end + +---@class cave.Python.Interpreter.Factory +---@field id_ string? +---@field path_ cave.Path? +---@field args_ string[] +---@field env_ table +---@field runnables_ cave.Python.Runnable.Factory[] +---@field context_ cave.Context +local Factory = Meta.derive "Python.Interpreter.Factory" + +---@param context cave.Context +function Factory:init(context) + self.args_ = {} + self.env_ = {} + self.runnables_ = {} + self.context_ = context +end + +---@param context cave.Context +---@return cave.Python.Interpreter.Factory +function Factory.new(context) + validate { context = { context, Context } } + local factory = setmetatable({}, Factory) + factory:init(context) + return factory +end + +---@return string +function Factory:get_id() return self.id_ or "python" end + +---@return cave.Path +function Factory:get_path() return self.path_ or Path.new "python" end + +---@return string[] +function Factory:get_args() return self.args_ end + +---@return table +function Factory:get_env() return self.env_ end + +---@return cave.Python.Runnable.Factory[] +function Factory:get_runnables() return self.runnables_ end + +---@param id string +---@return cave.Python.Interpreter.Factory +function Factory:id(id) + validate { id = { id, Str } } + self.id_ = id + return self +end + +---@param path_like cave.PathLike +---@return cave.Python.Interpreter.Factory +function Factory:path(path_like) + validate { path_like = { path_like, Path.Like } } + self.path_ = Path.like(path_like) + return self +end + +---@param args string[] +---@return cave.Python.Interpreter.Factory +function Factory:args(args) + validate { args = { args, List(Str) } } + vim.list_extend(self.args_, args) + return self +end + +---@param arg string +---@return cave.Python.Interpreter.Factory +function Factory:arg(arg) return self:args { arg } end + +---@param env table +---@return cave.Python.Interpreter.Factory +function Factory:env(env) + validate { env = { env, Map(Str, Str) } } + vim.tbl_extend("error", self.env_, env) + return self +end + +---@param name string +---@return cave.Python.Module.Factory +function Factory:module(name) + validate { name = { name, Str } } + local module_factory = Module.Factory.new(name, self.context_) + table.insert(self.runnables_, module_factory) + return module_factory +end + +---@param file_path_like cave.PathLike +---@return cave.Python.Script.Factory +function Factory:script(file_path_like) + validate { file = { file_path_like, Path.Like } } + local file = Path.like(file_path_like) + local script_factory = Script.Factory.new(file, self.context_) + table.insert(self.runnables_, script_factory) + return script_factory +end + +---@param runnables cave.Python.Runnable.Factory[] +---@return cave.Python.Interpreter.Factory +function Factory:runnables(runnables) + validate { runnables = { runnables, List(Runnable.Factory) } } + for _, runnable in ipairs(runnables) do + table.insert(self.runnables_, runnable:copy()) + end + return self +end + +---@param runnable cave.Python.Runnable.Factory +---@return cave.Python.Interpreter.Factory +function Factory:runnable(runnable) return self:runnables { runnable } end + +---@param interpreter cave.Python.Interpreter +function Factory:init_interpreter(interpreter) + interpreter.id = self:get_id() + interpreter.path = self:get_path() + interpreter.args = self:get_args() + interpreter.env = self:get_env() + interpreter.runnables = {} + for _, runnable_factory in pairs(self:get_runnables()) do + local runnable = runnable_factory:build() + assert(interpreter.runnables[runnable.id] == nil) + interpreter.runnables[runnable.id] = runnable + end +end + +---@return cave.Python.Interpreter +function Factory:build() + local interpreter = setmetatable({}, Interpreter) + self:init_interpreter(interpreter) + return interpreter +end + +Interpreter.Factory = Factory + +return Interpreter diff --git a/lua/cave/python/module.lua b/lua/cave/python/module.lua new file mode 100644 index 0000000..3c4c074 --- /dev/null +++ b/lua/cave/python/module.lua @@ -0,0 +1,69 @@ +local Runnable = require "cave.python.runnable" +local Context = require "cave.context" +local Meta = require "cave.meta" + +local Str = Meta.String +local validate = Meta.validate + +---@class cave.Python.Module : cave.Python.Runnable +---@field name string +local Module = Meta.derive("Python.Module", Runnable) + +function Module:module() return self end + +---@class cave.Python.Module.Factory : cave.Python.Runnable.Factory +---@field name_ string +local Factory = Meta.derive("Python.Module.Factory", Runnable.Factory) + +---@param name string +---@param context cave.Context +function Factory:init(name, context) + Runnable.Factory.init(self, context) + self.name_ = name +end + +---@param other cave.Python.Module.Factory +function Factory:copy_from(other) + Runnable.Factory.copy_from(self, other) + self.name_ = other.name_ +end + +---@param name string +---@param context cave.Context +---@return cave.Python.Module.Factory +function Factory.new(name, context) + validate { name = { name, Str }, context = { context, Context } } + local factory = setmetatable({}, Factory) + factory:init(name, context) + return factory +end + +---@return cave.Python.Module.Factory +function Factory:copy() + local factory = setmetatable({}, Factory) + factory:copy_from(self) + return factory +end + +---@return string +function Factory:get_id() return self.id_ or self:get_name() end + +---@return string +function Factory:get_name() return self.name_ end + +---@param module cave.Python.Module +function Factory:init_module(module) + self:init_runnable(module) + module.name = self:get_name() +end + +---@return cave.Python.Module +function Factory:build() + local module = setmetatable({}, Module) + self:init_module(module) + return module +end + +Module.Factory = Factory + +return Module diff --git a/lua/cave/python/runnable.lua b/lua/cave/python/runnable.lua new file mode 100644 index 0000000..ca11de7 --- /dev/null +++ b/lua/cave/python/runnable.lua @@ -0,0 +1,31 @@ +local Meta = require "cave.meta" +local Task = require "cave.task" + +---@class cave.Python.Runnable : cave.Task +local Runnable = Meta.derive("Python.Runnable", Task) + +---@return cave.Python.Script? +---@return cave.Python.Module? +function Runnable:concrete() return self:script(), self:module() end + +---@return cave.Python.Script? +function Runnable:script() end + +---@return cave.Python.Module? +function Runnable:module() end + +---@class cave.Python.Runnable.Factory : cave.Task.Factory +local Factory = Meta.derive("Python.Runnable.Factory", Task.Factory) + +---@return cave.Python.Runnable +function Factory:build() error "Not implemented" end + +---@return cave.Python.Runnable +function Factory:copy() error "Not implemented" end + +---@param runnable cave.Python.Runnable +function Factory:init_runnable(runnable) self:init_task(runnable) end + +Runnable.Factory = Factory + +return Runnable diff --git a/lua/cave/python/script.lua b/lua/cave/python/script.lua new file mode 100644 index 0000000..03c245f --- /dev/null +++ b/lua/cave/python/script.lua @@ -0,0 +1,76 @@ +local Runnable = require "cave.python.runnable" +local Meta = require "cave.meta" +local Path = require "cave.path" +local Context = require "cave.context" + +local validate = Meta.validate + +---@class cave.Python.Script : cave.Python.Runnable +---@field file cave.Path +local Script = Meta.derive("Python.Script", Runnable) + +---@return cave.Python.Script +function Script:script() return self end + +---@return string +function Script:valid_file() + assert(self.file:is_file()) + return self.file:tostring() +end + +---@class cave.Python.Script.Factory : cave.Python.Runnable.Factory +---@field file_ cave.Path +local Factory = Meta.derive("Python.Script.Factory", Runnable.Factory) + +---@param file cave.Path +---@param context cave.Context +function Factory:init(file, context) + Runnable.Factory.init(self, context) + self.file_ = file +end + +---@param other cave.Python.Script.Factory +function Factory:copy_from(other) + Runnable.Factory.copy_from(self, other) + self.file_ = other.file_:copy() +end + +---@param file cave.Path +---@param context cave.Context +---@return cave.Python.Script.Factory +function Factory.new(file, context) + validate { file = { file, Path }, context = { context, Context } } + local factory = setmetatable({}, Factory) + factory:init(file, context) + return factory +end + +---@return cave.Python.Script.Factory +function Factory:copy() + local factory = setmetatable({}, Factory) + factory:copy_from(self) + return factory +end + +---@return string +function Factory:get_id() return self.id_ or ("[%s]"):format(self:get_file()) end + +---@return cave.Path +function Factory:get_file() return self.file_ end + +---@param script cave.Python.Script +function Factory:init_script(script) + self:init_runnable(script) + script.file = self:get_file() +end + +---@return cave.Python.Script +function Factory:build() + local script = setmetatable({}, Script) + self:init_script(script) + return script +end + +Script.Factory = Factory + +return Script diff --git a/lua/cave/task.lua b/lua/cave/task.lua new file mode 100644 index 0000000..883b957 --- /dev/null +++ b/lua/cave/task.lua @@ -0,0 +1,124 @@ +local Path = require "cave.path" +local Meta = require "cave.meta" +local Enum = require "cave.enum" + +local List = Meta.List +local Map = Meta.Map +local Optional = Meta.Optional +local Str = Meta.String +local validate = Meta.validate + +---@enum cave.Task.Tag +local Tag = { + Run = "Run", + Debug = "Debug", +} +Enum.new(Tag, "Task.Tag") + +---@class cave.Task +---@field id string +---@field args string[] +---@field cwd cave.Path +---@field env table +local Task = Meta.derive "Task" + +Task.Tag = Tag + +---@return string +function Task:valid_cwd() + assert(self.cwd:is_dir()) + return self.cwd:tostring() +end + +---@class cave.Task.Factory +---@field id_ string? +---@field args_ string[] +---@field cwd_ cave.Path? +---@field env_ table +---@field context_ cave.Context +local Factory = Meta.derive "Task.Factory" + +---@param context cave.Context +function Factory:init(context) + self.args_ = {} + self.env_ = {} + self.context_ = context +end + +---@param other cave.Task.Factory +function Factory:copy_from(other) + self.id_ = other.id_ + self.args_ = vim.deepcopy(other.args_) + self.cwd_ = other.cwd_ and other.cwd_:copy() + self.env_ = vim.deepcopy(other.env_) + self.context_ = other.context_ +end + +---@return string +function Factory:get_id() + assert(self.id_) + return self.id_ +end + +---@return string[] +function Factory:get_args() return self.args_ end + +---@return cave.Path +function Factory:get_cwd() return self.cwd_ or Path.new(self.context_.dir) end + +---@return table +function Factory:get_env() return self.env_ end + +---@param id string +---@return cave.Task.Factory +function Factory:id(id) + validate { id = { id, Str } } + self.id_ = id + return self +end + +---@param args string[] +---@return cave.Task.Factory +function Factory:args(args) + validate { args = { args, List(Str) } } + vim.list_extend(self.args_, args) + return self +end + +---@param arg string +---@return cave.Task.Factory +function Factory:arg(arg) return self:args { arg } end + +---@param cwd_path_like cave.PathLike +---@return cave.Task.Factory +function Factory:cwd(cwd_path_like) + validate { cwd_path_like = { cwd_path_like, Optional(Path.Like) } } + self.cwd_ = Path.like(cwd_path_like) + return self +end + +---@param env table +---@return cave.Task.Factory +function Factory:env(env) + validate { env = { env, Map(Str, Str) } } + vim.tbl_extend("error", self.env_, env) + return self +end + +---@param task cave.Task +function Factory:init_task(task) + task.id = self:get_id() + task.args = self:get_args() + task.cwd = self:get_cwd() + task.env = self:get_env() +end + +---@return cave.Task +function Factory:build() error "Not implemented" end + +---@return cave.Task +function Factory:copy() error "Not implemented" end + +Task.Factory = Factory + +return Task diff --git a/lua/cave/template.lua b/lua/cave/template.lua new file mode 100644 index 0000000..e69de29 diff --git a/lua/cave/template_provider.lua b/lua/cave/template_provider.lua new file mode 100644 index 0000000..5da6c46 --- /dev/null +++ b/lua/cave/template_provider.lua @@ -0,0 +1,23 @@ +local Meta = require "cave.meta" +local ClassLogger = require "cave.log.class_logger" + +---@class cave.TemplateProvider : overseer.TemplateProvider +---@field get_templates fun(): overseer.TemplateDefinition[] +local TemplateProvider = Meta.derive "TemplateProvider" +TemplateProvider.log = ClassLogger.new(TemplateProvider) + +function TemplateProvider:init(name, get_templates_fn) + self.name = name + self.get_templates = get_templates_fn + ---@type fun(opts: overseer.SearchParams, cb: fun(tmpls: overseer.TemplateDefinition[])) + self.generator = function(_, cb) cb(self.get_templates()) end +end + +---@return cave.TemplateProvider +function TemplateProvider.new(name, get_templates_fn) + local provider = setmetatable({}, TemplateProvider) + provider:init(name, get_templates_fn) + return provider +end + +return TemplateProvider diff --git a/lua/cave/util.lua b/lua/cave/util.lua new file mode 100644 index 0000000..8531c9b --- /dev/null +++ b/lua/cave/util.lua @@ -0,0 +1,84 @@ +local Path = require "cave.path" +local Meta = require "cave.meta" + +local buffers = require "astrocore.buffer" + +local Table = Meta.Table +local validate = Meta.validate +local List = Meta.List +local Str = Meta.String + +local Util = {} + +---@param old table +---@param new table +---@return table +function Util.tbl_diff(old, new) + validate { old = { old, Table }, new = { new, Table } } + local diff = { + added = {}, + removed = {}, + modified = {}, + } + for old_name, old_value in pairs(old) do + local new_value = new[old_name] + if new_value == nil then + diff.removed[old_name] = old_value + elseif type(old_value) ~= type(new_value) or old_value ~= new_value then + diff.modified[old_name] = { old = old_value, new = new_value } + end + end + + for new_name, new_value in pairs(new) do + local old_value = old[new_name] + if old_value == nil then + diff.added[new_name] = new_value + elseif type(old_value) ~= type(new_value) or old_value ~= new_value then + assert(diff.modified[new_name].old == old_value and diff.modified[new_name].new == new_value) + end + end + return diff +end + +function Util.save_all_buffers() + for _, buf in ipairs(vim.t.bufs) do + local buf_valid = buffers.is_valid(buf) + local buf_modified = vim.api.nvim_buf_get_option(buf, "modified") + if not buf_valid or not buf_modified then goto continue end + local buf_name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), "%") + if buf_name == "" then goto continue end + local confirm = vim.fn.confirm(('Save changes to "%s"?'):format(buf_name), "&Yes\n&No", 1, "Question") + if confirm == 1 then vim.api.nvim_buf_call(buf, vim.cmd.write) end + ::continue:: + end +end + +function Util.close_all_buffers() buffers.close_all() end + +---@return string +function Util.generate_uuid() + local uuidgen_path = Path.new "uuidgen" + assert(uuidgen_path:is_executable()) + local cmd = { tostring(uuidgen_path), "-t" } + local output = vim.fn.systemlist(cmd) + validate { output = { output, List(Str) } } + assert(#output == 1) + return output[1] +end + +---@param t any +---@return any +function Util.plain(t) + if type(t) ~= "table" then return t end + local mt = getmetatable(t) + if mt ~= nil and (rawget(mt, "__tostring") ~= nil or rawget(mt, "__name") ~= nil) then + return tostring(t) + end + setmetatable(t, nil) + for key, value in pairs(t) do + t[key] = Util.plain(value) + end + return t +end + +return Util diff --git a/lua/overseer/component/dap.lua b/lua/overseer/component/dap.lua new file mode 100644 index 0000000..ddca388 --- /dev/null +++ b/lua/overseer/component/dap.lua @@ -0,0 +1,30 @@ +---@type overseer.ComponentFileDefinition +return { + desc = "DAP", + constructor = function(params) + ---@type Configuration + local config = params.config + ---@type overseer.ComponentSkeleton + return { + on_init = function(_, task) + task.cmd = { vim.fn.exepath "true" } + task:remove_component "on_complete_notify" + task:add_component "on_complete_dispose" + local ToggleTermStrategy = require "overseer.strategy.toggleterm" + task.strategy = ToggleTermStrategy.new { + auto_scroll = false, + close_on_exit = false, + quit_on_exit = "never", + hidden = true, + use_shell = false, + open_on_start = false, + } + end, + on_start = function() + local dap = require "dap" + dap.run(config, { new = true }) + end, + } + end, + params = { config = { type = "opaque" } }, +} diff --git a/mod/__pycache__/spawner.cpython-312.pyc b/mod/__pycache__/spawner.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9672937cd05e6a9db9f79b44f9d746c10a4cef2 GIT binary patch literal 157 zcmX@j%ge<81jWH-X(0MBh(HIQS%4zb87dhx8U0o=6fpsLpFwJV+308F=cekH?%C7Zl~E1BuMC#FA9~aeq6#e3Y#PYn