From 74008a8ac57b6e606b80317441b2ab0876153797 Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Mon, 15 May 2023 17:52:33 +0200 Subject: [PATCH 01/25] - added connection loaders from env and file - expand env variables from connection strings --- lua/dbee.lua | 22 ++++++++++++- lua/dbee/handler.lua | 2 +- lua/dbee/loader.lua | 70 +++++++++++++++++++++++++++++++++++++++++ lua/dbee/utils/init.lua | 21 +++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 lua/dbee/loader.lua diff --git a/lua/dbee.lua b/lua/dbee.lua index 1ab08e8e..be1e9028 100644 --- a/lua/dbee.lua +++ b/lua/dbee.lua @@ -79,12 +79,32 @@ function M.setup(o) pcall_lazy_setup() end +---@param from "file"|"env" +---@param opt? string path to file or environment variable +function M.load_connections(from, opt) + if not pcall_lazy_setup() then + return + end + + local conns = {} + local loader = require("dbee.loader") + if from == "file" then + conns = loader.from_file(opt) + elseif from == "env" then + conns = loader.from_env(opt) + end + + for _, conn in ipairs(conns) do + m.handler:add_connection(utils.expand_environmet(conn) --[[@as connection_details]]) + end +end + ---@param connection connection_details function M.add_connection(connection) if not pcall_lazy_setup() then return end - m.handler:add_connection(connection) + m.handler:add_connection(utils.expand_environmet(connection) --[[@as connection_details]]) end function M.open() diff --git a/lua/dbee/handler.lua b/lua/dbee/handler.lua index d3259a0b..0e5844e5 100644 --- a/lua/dbee/handler.lua +++ b/lua/dbee/handler.lua @@ -102,7 +102,7 @@ function Handler:add_connection(connection) end connection.name = connection.name or "[empty name]" - + connection.type = utils.type_alias(connection.type) connection.id = connection.name .. connection.type -- register in go diff --git a/lua/dbee/loader.lua b/lua/dbee/loader.lua new file mode 100644 index 00000000..aa0452ff --- /dev/null +++ b/lua/dbee/loader.lua @@ -0,0 +1,70 @@ +local utils = require("dbee.utils") + +local M = {} + +-- Parses json file with connections +---@param path? string path to file +---@return connection_details[] +function M.from_file(path) + path = path or vim.fn.getcwd() .. "/.vim/dbee.json" + + ---@type connection_details[] + local conns = {} + + if not vim.loop.fs_stat(path) then + return {} + end + + local lines = {} + for line in io.lines(path) do + if not vim.startswith(vim.trim(line), "//") then + table.insert(lines, line) + end + end + + local contents = table.concat(lines, "\n") + local ok, data = pcall(vim.fn.json_decode, contents) + if not ok then + utils.log("error", 'Could not parse json file: "' .. path .. '".', "loader") + return {} + end + + for _, conn in pairs(data) do + if type(conn) == "table" and conn.url and conn.type then + table.insert(conns, conn) + end + end + + return conns +end + +-- Parses env variable if it exists +---@param var? string env var to check - default: DBEE_CONNECTIONS +---@return connection_details[] +function M.from_env(var) + var = var or "DBEE_CONNECTIONS" + + ---@type connection_details[] + local conns = {} + + local raw = os.getenv(var) + if not raw then + return {} + end + + local ok, data = pcall(vim.fn.json_decode, raw) + if not ok then + utils.log("error", 'Could not parse connections from env: "' .. var .. '".', "loader") + return {} + end + + for _, conn in pairs(data) do + if type(conn) == "table" and conn.url and conn.type then + table.insert(conns, conn) + end + end + + return conns +end + +return M diff --git a/lua/dbee/utils/init.lua b/lua/dbee/utils/init.lua index 210df806..bf2e4158 100644 --- a/lua/dbee/utils/init.lua +++ b/lua/dbee/utils/init.lua @@ -72,4 +72,25 @@ function M.log(level, message, subtitle) vim.notify(subtitle .. " " .. message, l, { title = "nvim-dbee" }) end +-- Replaces {{ env.SOMETHING }} with environment or empty string +---@param obj string|table +---@return string|table +function M.expand_environmet(obj) + local function expand(o) + if type(o) ~= "string" then + return o + end + local ret = o:gsub("{{%s*env.([%w_]*)%s*}}", function(v) + return os.getenv(v) or "" + end) + return ret + end + + if type(obj) == "table" then + return vim.tbl_map(expand, obj) + end + + return expand(obj) +end + return M From 01af8bdfaf0c480c9ed3f82ee0c6511235b969e7 Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Mon, 15 May 2023 19:58:24 +0200 Subject: [PATCH 02/25] added "new connection" prompt to drawer --- dbee/clients/mongo.go | 2 +- lua/dbee/drawer.lua | 29 +++++++++++++ lua/dbee/handler.lua | 8 ++-- lua/dbee/prompt.lua | 98 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 lua/dbee/prompt.lua diff --git a/dbee/clients/mongo.go b/dbee/clients/mongo.go index 2af09d32..0bcadf02 100644 --- a/dbee/clients/mongo.go +++ b/dbee/clients/mongo.go @@ -57,7 +57,7 @@ func NewMongo(url string) (*MongoClient, error) { // get database name from url dbName, err := getDatabaseName(url) if err != nil { - return nil, fmt.Errorf("mongo: invalid url: %s -- %v", url, err) + return nil, fmt.Errorf("mongo: invalid url: %v", err) } opts := options.Client().ApplyURI(url) diff --git a/lua/dbee/drawer.lua b/lua/dbee/drawer.lua index 3cca7f67..4c2cef7e 100644 --- a/lua/dbee/drawer.lua +++ b/lua/dbee/drawer.lua @@ -1,7 +1,9 @@ local NuiTree = require("nui.tree") local NuiLine = require("nui.line") +local utils = require("dbee.utils") local SCRATCHPAD_NODE_ID = "scratchpad_node" +local ADD_CONNECTION_NODE_ID = "add_connection_node" ---@class Icon ---@field icon string @@ -350,6 +352,32 @@ function Drawer:refresh() self.tree:add_node(node) end + -- new connection + if not exists(ADD_CONNECTION_NODE_ID) then + ---@type MasterNode + local node = NuiTree.Node { + id = ADD_CONNECTION_NODE_ID, + text = "- add connection -", + type = "", + is_master = true, + action_1 = function() + local prompt = { + "name", + "type", + "url", + } + require("dbee.prompt").open(prompt, { + title = "Add Connection", + callback = function(result) + self.handler:add_connection(utils.expand_environmet(result) --[[@as connection_details]]) + self:refresh() + end, + }) + end, + } + self.tree:add_node(node) + end + -- connections local cons = self.handler:list_connections() for _, con in ipairs(cons) do @@ -379,6 +407,7 @@ function Drawer:refresh() self:refresh_node(n.id) end end + self.tree:render() end -- Show drawer on screen diff --git a/lua/dbee/handler.lua b/lua/dbee/handler.lua index 0e5844e5..dc821621 100644 --- a/lua/dbee/handler.lua +++ b/lua/dbee/handler.lua @@ -226,7 +226,7 @@ function Handler:layout(id) return {} end - local _new_layouts = {} + local new_layouts = {} for _, lgo in ipairs(layout_go) do -- action 1 executes query or history local action_1 @@ -270,7 +270,7 @@ function Handler:layout(id) end -- action_3 is empty - local _ly = { + local ly = { name = lgo.name, schema = lgo.schema, database = lgo.database, @@ -281,10 +281,10 @@ function Handler:layout(id) children = to_layout(lgo.children), } - table.insert(_new_layouts, _ly) + table.insert(new_layouts, ly) end - return _new_layouts + return new_layouts end return to_layout(vim.fn.json_decode(vim.fn.Dbee_layout(id))) diff --git a/lua/dbee/prompt.lua b/lua/dbee/prompt.lua new file mode 100644 index 00000000..d60a2fc6 --- /dev/null +++ b/lua/dbee/prompt.lua @@ -0,0 +1,98 @@ +local M = {} + +---@param prompt string[] list of lines to display as prompt +---@param opts? { width: integer, height: integer, title: string, border: string|string[], callback: fun(result: table) } optional parameters +function M.open(prompt, opts) + opts = opts or {} + + -- add colons to prompt + for i, p in ipairs(prompt) do + if not p:find(":$") then + prompt[i] = p .. ":" + end + end + + local win_width = opts.width or 100 + local win_height = opts.height or #prompt + local ui_spec = vim.api.nvim_list_uis()[1] + local x = math.floor((ui_spec["width"] - win_width) / 2) + local y = math.floor((ui_spec["height"] - win_height) / 2) + + -- create new buffer + local bufnr = vim.api.nvim_create_buf(false, false) + local name = opts.title or tostring(os.clock()) + vim.api.nvim_buf_set_name(bufnr, name) + vim.api.nvim_buf_set_option(bufnr, "filetype", "dbee") + vim.api.nvim_buf_set_option(bufnr, "buftype", "acwrite") + vim.api.nvim_buf_set_option(bufnr, "bufhidden", "delete") + + -- fill buffer contents + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, prompt) + vim.api.nvim_buf_set_option(bufnr, "modified", false) + + -- open window + local winid = vim.api.nvim_open_win(bufnr, true, { + relative = "editor", + width = win_width, + height = win_height, + col = x, + row = y, + border = opts.border or "rounded", + title = opts.title or "", + title_pos = "center", + style = "minimal", + }) + + local callback = opts.callback or function() end + + -- set callbacks + vim.api.nvim_create_autocmd("BufWriteCmd", { + buffer = bufnr, + callback = function() + -- reset modified flag + vim.api.nvim_buf_set_option(bufnr, "modified", false) + + -- get lines + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + + -- close the window if not using "wq" already + local cmd_hist = vim.api.nvim_exec2(":history cmd -1", { output = true }) + local last_cmd = cmd_hist.output:gsub(".*\n>%s*%d+%s*(.*)%s*", "%1") + if not last_cmd:find("^wq") then + vim.api.nvim_win_close(winid, true) + end + + -- create key-value from prompt and values and trigger callback + local kv = {} + for _, p in ipairs(prompt) do + -- get key from prompt and store it as empty string by default + local key = p:gsub("(.*):", "%1") + kv[key] = "" + + for _, l in ipairs(lines) do + -- if line has prompt prefix, get the value and strip whitespace + if l:find("^%s*" .. p) then + local val = l:gsub("^%s*" .. p .. "%s*(.-)%s*$", "%1") + kv[key] = val + end + end + end + + callback(kv) + end, + }) + + vim.api.nvim_create_autocmd({ "BufLeave", "BufWritePost" }, { + buffer = bufnr, + callback = function() + vim.api.nvim_win_close(winid, true) + end, + }) + + -- set keymaps + vim.keymap.set("n", "q", function() + vim.api.nvim_win_close(winid, true) + end, { silent = true, buffer = bufnr }) +end + +return M From 446b650c95bb5485a98fe07230bc705265127e7b Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Tue, 16 May 2023 18:19:37 +0200 Subject: [PATCH 03/25] - layout to tree node mapping almost 1:1 - moved layout to editor and handler - node/layout organization more uniform --- lua/dbee/drawer.lua | 219 ++++++++++++------------------------------- lua/dbee/editor.lua | 10 +- lua/dbee/handler.lua | 64 +++++++++++-- 3 files changed, 129 insertions(+), 164 deletions(-) diff --git a/lua/dbee/drawer.lua b/lua/dbee/drawer.lua index 4c2cef7e..b9970ac3 100644 --- a/lua/dbee/drawer.lua +++ b/lua/dbee/drawer.lua @@ -1,35 +1,23 @@ local NuiTree = require("nui.tree") local NuiLine = require("nui.line") -local utils = require("dbee.utils") - -local SCRATCHPAD_NODE_ID = "scratchpad_node" -local ADD_CONNECTION_NODE_ID = "add_connection_node" ---@class Icon ---@field icon string ---@field highlight string ---@class Layout +---@field id string unique identifier ---@field name string display name +---@field type ""|"table"|"history"|"scratch"|"database" type of layout ---@field schema? string parent schema ---@field database? string parent database ----@field type ""|"table"|"history"|"scratch"|"database" type of layout ---@field action_1? fun(cb: fun()) primary action - takes single arg: callback closure ---@field action_2? fun(cb: fun()) secondary action - takes single arg: callback closure ---@field action_3? fun(cb: fun()) tertiary action - takes single arg: callback closure ----@field children? Layout[] child layout nodes - ----@class Node ----@field id string ----@field text string ----@field type string type which infers icon ----@field is_expanded fun(self:Node):boolean ----@field is_master boolean ----@field action_1 fun() function to perform on primary event ----@field action_2 fun() function to perform on secondary event ----@field action_3 fun() function to perform on tertiary event - ----@class MasterNode: Node +---@field children? Layout[]|fun():Layout[] child layout nodes + +-- node is Layout converted to NuiTreeNode +---@class Node: Layout ---@field getter fun():Layout ---@alias drawer_config { disable_icons: boolean, icons: table, mappings: table, window_command: string|fun():integer } @@ -133,10 +121,10 @@ function Drawer:create_tree(bufnr) -- if connection is the active one, apply a special highlight on the master local active = self.handler:connection_details() - if node.is_master and active and active.id == node.id then - line:append(node.text, icon.highlight) + if active and active.id == node.id then + line:append(node.name, icon.highlight) else - line:append(node.text) + line:append(node.name) end return line @@ -173,8 +161,9 @@ function Drawer:actions() expand_all_single(node) - if node.is_master then - self:refresh_node(node.id) + -- if function for getting layout exist, call it + if not expanded and type(node.getter) == "function" then + node.getter() end node:expand() @@ -191,19 +180,25 @@ function Drawer:actions() action_1 = function() local node = self.tree:get_node() if type(node.action_1) == "function" then - node.action_1() + node.action_1(function() + self:refresh() + end) end end, action_2 = function() local node = self.tree:get_node() if type(node.action_2) == "function" then - node.action_2() + node.action_2(function() + self:refresh() + end) end end, action_3 = function() local node = self.tree:get_node() if type(node.action_3) == "function" then - node.action_3() + node.action_3(function() + self:refresh() + end) end end, collapse = function() @@ -250,59 +245,54 @@ function Drawer:map_keys(bufnr) end end +-- sets layout to tree ---@private ----@param master_node_id string master node id -function Drawer:refresh_node(master_node_id) +---@param layout Layout[] layout to add to tree +---@param node_id? string layout is set as children to this id or root +function Drawer:set_layout(layout, node_id) + --- recursed over Layout[] and sets it to the tree ---@param layouts Layout[] - ---@param parent_id? string - ---@return table nodes list of NuiTreeNodes - local function layout_to_tree_nodes(layouts, parent_id) - parent_id = parent_id or "" - + ---@return Node[] nodes list of NuiTreeNodes + local function to_node(layouts) if not layouts then return {} end - -- sort keys - table.sort(layouts, function(k1, k2) - return k1.name < k2.name - end) - local nodes = {} for _, l in ipairs(layouts) do - local id = parent_id .. l.name - local node = NuiTree.Node({ - id = id, - master_id = master_node_id, - text = string.gsub(l.name, "\n", " "), - type = l.type, - action_1 = function() - if type(l.action_1) == "function" then - l.action_1(function() - self:refresh() - end) - end - end, - action_2 = function() - if type(l.action_2) == "function" then - l.action_2(function() - self:refresh() - end) - end - end, - action_3 = function() - if type(l.action_3) == "function" then - l.action_3(function() - self:refresh() - end) + + -- get children or set getter + local getter + local children + if type(l.children) == "function" then + getter = function() + local exists = self.tree:get_node(l.id) + if exists then + self.tree:set_nodes(to_node(l.children()), l.id) end - end, - -- recurse children - }, layout_to_tree_nodes(l.children, id)) + end + else + children = l.children + end + + -- all other fields stay the same + local n = vim.fn.copy(l) + n.name = string.gsub(l.name, "\n", " ") + n.getter = getter -- get existing node from the current tree and check if it is expanded - local ex_node = self.tree:get_node(id) + local expanded = false + local ex_node = self.tree:get_node(l.id) if ex_node and ex_node:is_expanded() then + expanded = true + -- if getter exists, and node is expanded, we call it + if getter then + children = l.children() + end + end + -- recurse children + local node = NuiTree.Node(n, to_node(children --[[@as Layout[] ]])) + if expanded then node:expand() end @@ -312,101 +302,16 @@ function Drawer:refresh_node(master_node_id) return nodes end - ---@type MasterNode - local master_node = self.tree:get_node(master_node_id) - - local layout = master_node.getter() - - local children = layout_to_tree_nodes(layout, tostring(master_node_id)) - - self.tree:set_nodes(children, master_node_id) - self.tree:render() + -- recurse layout + self.tree:set_nodes(to_node(layout), node_id) end function Drawer:refresh() - ---@type MasterNode[] - local existing_nodes = self.tree:get_nodes() - - ---@param id string - local function exists(id) - for _, n in ipairs(existing_nodes) do - if n.id == id then - return true - end - end - return false - end - - -- scratchpads - if not exists(SCRATCHPAD_NODE_ID) then - ---@type MasterNode - local node = NuiTree.Node { - id = SCRATCHPAD_NODE_ID, - text = "scratchpads", - type = "scratch", - is_master = true, - getter = function() - return self.editor:layout() - end, - } - self.tree:add_node(node) - end - - -- new connection - if not exists(ADD_CONNECTION_NODE_ID) then - ---@type MasterNode - local node = NuiTree.Node { - id = ADD_CONNECTION_NODE_ID, - text = "- add connection -", - type = "", - is_master = true, - action_1 = function() - local prompt = { - "name", - "type", - "url", - } - require("dbee.prompt").open(prompt, { - title = "Add Connection", - callback = function(result) - self.handler:add_connection(utils.expand_environmet(result) --[[@as connection_details]]) - self:refresh() - end, - }) - end, - } - self.tree:add_node(node) - end + ---@type Layout[] + local layouts = { unpack(self.editor:layout()), unpack(self.handler:layout()) } - -- connections - local cons = self.handler:list_connections() - for _, con in ipairs(cons) do - if not exists(con.id) then - ---@type MasterNode - local node = NuiTree.Node { - id = con.id, - text = con.name, - type = "database", - is_master = true, - -- set connection as active manually - action_2 = function() - self.handler:set_active(con.id) - self:refresh_node(con.id) - end, - getter = function() - return self.handler:layout(con.id) - end, - } - self.tree:add_node(node) - end - end + self:set_layout(layouts) - -- refresh open master nodes - for _, n in ipairs(existing_nodes) do - if n:is_expanded() then - self:refresh_node(n.id) - end - end self.tree:render() end diff --git a/lua/dbee/editor.lua b/lua/dbee/editor.lua index 1567a301..d13442ac 100644 --- a/lua/dbee/editor.lua +++ b/lua/dbee/editor.lua @@ -155,7 +155,9 @@ function Editor:layout() ---@type Layout[] local scratches = { { + id = "__new_scratchpad__", name = "- new -", + type = "", action_1 = function(cb) self:new_scratch() self:open() @@ -167,6 +169,7 @@ function Editor:layout() for _, s in pairs(self.scratches) do ---@type Layout local sch = { + id = "__scratchpad_" .. s.file .. "__", name = vim.fs.basename(s.file), type = "scratch", action_1 = function(cb) @@ -198,7 +201,12 @@ function Editor:layout() table.insert(scratches, sch) end - return scratches + return { { + id = "__master_scratchpad__", + name = "scratchpads", + type = "scratch", + children = scratches, + } } end ---@param id scratch_id scratch id - name diff --git a/lua/dbee/handler.lua b/lua/dbee/handler.lua index dc821621..bbd86e64 100644 --- a/lua/dbee/handler.lua +++ b/lua/dbee/handler.lua @@ -48,7 +48,7 @@ function Handler:new(connections, opts) conn.type = utils.type_alias(conn.type) conn.name = conn.name or "[empty name]" - local id = conn.name .. conn.type + local id = "__master_connection_id_" .. conn.name .. conn.type .. "__" conn.id = id if id < active then @@ -103,7 +103,7 @@ function Handler:add_connection(connection) connection.name = connection.name or "[empty name]" connection.type = utils.type_alias(connection.type) - connection.id = connection.name .. connection.type + connection.id = "__master_connection_id_" .. connection.name .. connection.type .. "__" -- register in go vim.fn.Dbee_register_connection(connection.id, connection.url, connection.type, tostring(self.page_size)) @@ -214,18 +214,24 @@ function Handler:history(history_id, id) end -- get layout for the connection +---@private ---@param id? conn_id connection id ---@return Layout[] -function Handler:layout(id) +function Handler:get_connection_layout(id) id = id or self.active_connection ---@param layout_go _LayoutGo[] layout from go ---@return Layout[] layout with actions - local function to_layout(layout_go) + local function to_layout(layout_go, parent_id) if not layout_go or layout_go == vim.NIL then return {} end + -- sort keys + table.sort(layout_go, function(k1, k2) + return k1.name < k2.name + end) + local new_layouts = {} for _, lgo in ipairs(layout_go) do -- action 1 executes query or history @@ -270,7 +276,9 @@ function Handler:layout(id) end -- action_3 is empty + local l_id = (parent_id or "") .. "__connection_" .. lgo.name .. lgo.schema .. lgo.type .. "__" local ly = { + id = l_id, name = lgo.name, schema = lgo.schema, database = lgo.database, @@ -278,7 +286,7 @@ function Handler:layout(id) action_1 = action_1, action_2 = action_2, action_3 = nil, - children = to_layout(lgo.children), + children = to_layout(lgo.children, l_id), } table.insert(new_layouts, ly) @@ -287,7 +295,51 @@ function Handler:layout(id) return new_layouts end - return to_layout(vim.fn.json_decode(vim.fn.Dbee_layout(id))) + return to_layout(vim.fn.json_decode(vim.fn.Dbee_layout(id)), id) +end + +---@return Layout[] +function Handler:layout() + ---@type Layout[] + local layout = {} + + for _, conn in ipairs(self:list_connections()) do + table.insert(layout, { + id = conn.id, + name = conn.name, + type = "database", + -- set connection as active manually + action_2 = function(cb) + self:set_active(conn.id) + cb() + end, + children = function() + return self:get_connection_layout(conn.id) + end, + }) + end + + table.insert(layout, { + id = "__add_connection__", + name = "- add connection -", + type = "", + action_1 = function(cb) + local prompt = { + "name", + "type", + "url", + } + require("dbee.prompt").open(prompt, { + title = "Add Connection", + callback = function(result) + self:add_connection(utils.expand_environmet(result) --[[@as connection_details]]) + cb() + end, + }) + end, + }) + + return layout end ---@param format "csv"|"json" how to format the result From e946f9aaa02298bd97dfc6d5de42865733296bf6 Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Tue, 16 May 2023 19:04:40 +0200 Subject: [PATCH 04/25] - made tree prettier - fix non refreshing bug - if go doesn't register, don't register in lua either --- dbee/main.go | 20 ++++++++--------- lua/dbee/config.lua | 12 ++++++++++ lua/dbee/drawer.lua | 53 ++++++++++++++++++++++++++++++++++++++------ lua/dbee/editor.lua | 4 ++-- lua/dbee/handler.lua | 11 +++++---- 5 files changed, 77 insertions(+), 23 deletions(-) diff --git a/dbee/main.go b/dbee/main.go index 85f7bc54..1a1a4821 100644 --- a/dbee/main.go +++ b/dbee/main.go @@ -66,12 +66,12 @@ func main() { }) p.HandleFunction(&plugin.FunctionOptions{Name: "Dbee_register_connection"}, - func(args []string) error { + func(args []string) (bool, error) { method := "Dbee_register_connection" logger.Debug("calling " + method) if len(args) < 4 { logger.Error("not enough arguments passed to " + method) - return nil + return false, nil } id := args[0] @@ -80,7 +80,7 @@ func main() { pageSize, err := strconv.Atoi(args[3]) if err != nil { logger.Error(err.Error()) - return nil + return false, nil } // Get the right client @@ -90,35 +90,35 @@ func main() { client, err = clients.NewPostgres(url) if err != nil { logger.Error(err.Error()) - return nil + return false, nil } case "mysql": client, err = clients.NewMysql(url) if err != nil { logger.Error(err.Error()) - return nil + return false, nil } case "sqlite": client, err = clients.NewSqlite(url) if err != nil { logger.Error(err.Error()) - return nil + return false, nil } case "redis": client, err = clients.NewRedis(url) if err != nil { logger.Error(err.Error()) - return nil + return false, nil } case "mongo": client, err = clients.NewMongo(url) if err != nil { logger.Error(err.Error()) - return nil + return false, nil } default: logger.Error("database of type \"" + typ + "\" is not supported") - return nil + return false, nil } h := conn.NewHistory(id, logger) @@ -128,7 +128,7 @@ func main() { connections[id] = c logger.Debug(method + " returned successfully") - return nil + return true, nil }) p.HandleFunction(&plugin.FunctionOptions{Name: "Dbee_execute"}, diff --git a/lua/dbee/config.lua b/lua/dbee/config.lua index 11cc640d..c2d9d7c5 100644 --- a/lua/dbee/config.lua +++ b/lua/dbee/config.lua @@ -88,6 +88,18 @@ M.default = { icon = "", highlight = "Conditional", }, + add = { + icon = "", + highlight = "String", + }, + remove = { + icon = "󰆴", + highlight = "SpellBad", + }, + help = { + icon = "󰋖", + highlight = "NormalFloat", + }, -- if there is no type -- use this for normal nodes... diff --git a/lua/dbee/drawer.lua b/lua/dbee/drawer.lua index b9970ac3..34378413 100644 --- a/lua/dbee/drawer.lua +++ b/lua/dbee/drawer.lua @@ -8,7 +8,7 @@ local NuiLine = require("nui.line") ---@class Layout ---@field id string unique identifier ---@field name string display name ----@field type ""|"table"|"history"|"scratch"|"database" type of layout +---@field type ""|"table"|"history"|"scratch"|"database"|"add"|"remove"|"help" type of layout ---@field schema? string parent schema ---@field database? string parent database ---@field action_1? fun(cb: fun()) primary action - takes single arg: callback closure @@ -91,7 +91,7 @@ function Drawer:create_tree(bufnr) line:append(string.rep(" ", node:get_depth() - 1)) - if node:has_children() or not node:get_parent_id() then + if node:has_children() or node.getter then local icon = self.icons["node_closed"] or { icon = ">", highlight = "NonText" } if node:is_expanded() then icon = self.icons["node_expanded"] or { icon = "v", highlight = "NonText" } @@ -168,9 +168,7 @@ function Drawer:actions() node:expand() - if expanded ~= node:is_expanded() then - self.tree:render() - end + self.tree:render() end return { @@ -260,7 +258,6 @@ function Drawer:set_layout(layout, node_id) local nodes = {} for _, l in ipairs(layouts) do - -- get children or set getter local getter local children @@ -307,8 +304,50 @@ function Drawer:set_layout(layout, node_id) end function Drawer:refresh() + -- whitespace between nodes + ---@return Layout + local seperator = function() + return { + id = "__seperator_layout__" .. tostring(math.random()), + name = "", + type = "", + } + end + + -- help node + local help_children = {} + for act, map in pairs(self.mappings) do + table.insert(help_children, { + id = "__help_action_" .. act, + name = act .. " = " .. map.key .. " (" .. map.mode .. ")", + type = "", + }) + end + + table.sort(help_children, function(k1, k2) + return k1.id < k2.id + end) + + ---@type Layout + local help = { + id = "__help_layout__", + name = "help", + type = "help", + children = help_children, + } + + -- assemble tree layout ---@type Layout[] - local layouts = { unpack(self.editor:layout()), unpack(self.handler:layout()) } + local layouts = {} + for _, ly in ipairs(self.editor:layout()) do + table.insert(layouts, ly) + end + table.insert(layouts, seperator()) + for _, ly in ipairs(self.handler:layout()) do + table.insert(layouts, ly) + end + table.insert(layouts, seperator()) + table.insert(layouts, help) self:set_layout(layouts) diff --git a/lua/dbee/editor.lua b/lua/dbee/editor.lua index d13442ac..a9d162df 100644 --- a/lua/dbee/editor.lua +++ b/lua/dbee/editor.lua @@ -156,8 +156,8 @@ function Editor:layout() local scratches = { { id = "__new_scratchpad__", - name = "- new -", - type = "", + name = "new", + type = "add", action_1 = function(cb) self:new_scratch() self:open() diff --git a/lua/dbee/handler.lua b/lua/dbee/handler.lua index bbd86e64..60419683 100644 --- a/lua/dbee/handler.lua +++ b/lua/dbee/handler.lua @@ -96,7 +96,7 @@ function Handler:add_connection(connection) error("url needs to be set!") return end - if not connection.type then + if not connection.type or connection.type == "" then error("no type") return end @@ -106,7 +106,10 @@ function Handler:add_connection(connection) connection.id = "__master_connection_id_" .. connection.name .. connection.type .. "__" -- register in go - vim.fn.Dbee_register_connection(connection.id, connection.url, connection.type, tostring(self.page_size)) + local ok = vim.fn.Dbee_register_connection(connection.id, connection.url, connection.type, tostring(self.page_size)) + if not ok then + return + end self.connections[connection.id] = connection self.active_connection = connection.id @@ -321,8 +324,8 @@ function Handler:layout() table.insert(layout, { id = "__add_connection__", - name = "- add connection -", - type = "", + name = "add connection", + type = "add", action_1 = function(cb) local prompt = { "name", From 70261054a1fc019472edc8eabc36ec36d28a1038 Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Tue, 16 May 2023 19:47:32 +0200 Subject: [PATCH 05/25] - persist added connections in a file - fix typos - add prompt mapping --- dbee/conn/cache.go | 2 +- lua/dbee/drawer.lua | 8 ++++---- lua/dbee/handler.lua | 9 +++++---- lua/dbee/loader.lua | 36 +++++++++++++++++++++++++++++++++--- lua/dbee/prompt.lua | 6 ++++++ 5 files changed, 49 insertions(+), 12 deletions(-) diff --git a/dbee/conn/cache.go b/dbee/conn/cache.go index ece97281..ab9c095e 100644 --- a/dbee/conn/cache.go +++ b/dbee/conn/cache.go @@ -106,7 +106,7 @@ func (c *cache) set(iter models.IterResult) error { }) c.active = id - // process everything else in a seperate goroutine + // process everything else in a separate goroutine if !drained { go func() { i := 0 diff --git a/lua/dbee/drawer.lua b/lua/dbee/drawer.lua index 34378413..5fc77ba3 100644 --- a/lua/dbee/drawer.lua +++ b/lua/dbee/drawer.lua @@ -306,9 +306,9 @@ end function Drawer:refresh() -- whitespace between nodes ---@return Layout - local seperator = function() + local separator = function() return { - id = "__seperator_layout__" .. tostring(math.random()), + id = "__separator_layout__" .. tostring(math.random()), name = "", type = "", } @@ -342,11 +342,11 @@ function Drawer:refresh() for _, ly in ipairs(self.editor:layout()) do table.insert(layouts, ly) end - table.insert(layouts, seperator()) + table.insert(layouts, separator()) for _, ly in ipairs(self.handler:layout()) do table.insert(layouts, ly) end - table.insert(layouts, seperator()) + table.insert(layouts, separator()) table.insert(layouts, help) self:set_layout(layouts) diff --git a/lua/dbee/handler.lua b/lua/dbee/handler.lua index 60419683..12889132 100644 --- a/lua/dbee/handler.lua +++ b/lua/dbee/handler.lua @@ -94,11 +94,9 @@ end function Handler:add_connection(connection) if not connection.url then error("url needs to be set!") - return end if not connection.type or connection.type == "" then error("no type") - return end connection.name = connection.name or "[empty name]" @@ -108,7 +106,7 @@ function Handler:add_connection(connection) -- register in go local ok = vim.fn.Dbee_register_connection(connection.id, connection.url, connection.type, tostring(self.page_size)) if not ok then - return + error("problem adding connection") end self.connections[connection.id] = connection @@ -335,7 +333,10 @@ function Handler:layout() require("dbee.prompt").open(prompt, { title = "Add Connection", callback = function(result) - self:add_connection(utils.expand_environmet(result) --[[@as connection_details]]) + local ok = pcall(self.add_connection, self, utils.expand_environmet(result) --[[@as connection_details]]) + if ok then + require("dbee.loader").to_file { result } + end cb() end, }) diff --git a/lua/dbee/loader.lua b/lua/dbee/loader.lua index aa0452ff..142969ff 100644 --- a/lua/dbee/loader.lua +++ b/lua/dbee/loader.lua @@ -1,12 +1,14 @@ local utils = require("dbee.utils") +local DEFAULT_PERSISTENCE_FILE = vim.fn.stdpath("cache") .. "/dbee/persistence.json" + local M = {} -- Parses json file with connections ---@param path? string path to file ---@return connection_details[] function M.from_file(path) - path = path or vim.fn.getcwd() .. "/.vim/dbee.json" + path = path or DEFAULT_PERSISTENCE_FILE ---@type connection_details[] local conns = {} @@ -25,7 +27,7 @@ function M.from_file(path) local contents = table.concat(lines, "\n") local ok, data = pcall(vim.fn.json_decode, contents) if not ok then - utils.log("error", 'Could not parse json file: "' .. path .. '".', "loader") + utils.log("warn", 'Could not parse json file: "' .. path .. '".', "loader") return {} end @@ -54,7 +56,7 @@ function M.from_env(var) local ok, data = pcall(vim.fn.json_decode, raw) if not ok then - utils.log("error", 'Could not parse connections from env: "' .. var .. '".', "loader") + utils.log("warn", 'Could not parse connections from env: "' .. var .. '".', "loader") return {} end @@ -67,4 +69,32 @@ function M.from_env(var) return conns end +-- saves connection_details to a file +---@param connections connection_details[] +---@param path? string path to save file +function M.to_file(connections, path) + path = path or DEFAULT_PERSISTENCE_FILE + + if not connections or vim.tbl_isempty(connections) then + return + end + + local existing = M.from_file(path) + + for _, conn in ipairs(connections) do + table.insert(existing, conn) + end + + local ok, json = pcall(vim.fn.json_encode, existing) + if not ok then + utils.log("error", "Could not convert connection list to json", "loader") + return + end + + -- overwrite file + local file = assert(io.open(path, "w+"), "could not open file") + file:write(json) + file:close() +end + return M diff --git a/lua/dbee/prompt.lua b/lua/dbee/prompt.lua index d60a2fc6..47781fcc 100644 --- a/lua/dbee/prompt.lua +++ b/lua/dbee/prompt.lua @@ -93,6 +93,12 @@ function M.open(prompt, opts) vim.keymap.set("n", "q", function() vim.api.nvim_win_close(winid, true) end, { silent = true, buffer = bufnr }) + + vim.keymap.set("i", "", function() + -- write and return to normal mode + vim.cmd(":w") + vim.api.nvim_input("") + end, { silent = true, buffer = bufnr }) end return M From dc108b1c0ca7dd689926868ad3da46403ef3abdb Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Tue, 16 May 2023 20:08:11 +0200 Subject: [PATCH 06/25] moved prompt to utils module --- lua/dbee/handler.lua | 2 +- lua/dbee/utils/init.lua | 3 +++ lua/dbee/{ => utils}/prompt.lua | 0 3 files changed, 4 insertions(+), 1 deletion(-) rename lua/dbee/{ => utils}/prompt.lua (100%) diff --git a/lua/dbee/handler.lua b/lua/dbee/handler.lua index 12889132..77cf9e02 100644 --- a/lua/dbee/handler.lua +++ b/lua/dbee/handler.lua @@ -330,7 +330,7 @@ function Handler:layout() "type", "url", } - require("dbee.prompt").open(prompt, { + utils.prompt.open(prompt, { title = "Add Connection", callback = function(result) local ok = pcall(self.add_connection, self, utils.expand_environmet(result) --[[@as connection_details]]) diff --git a/lua/dbee/utils/init.lua b/lua/dbee/utils/init.lua index bf2e4158..e5018750 100644 --- a/lua/dbee/utils/init.lua +++ b/lua/dbee/utils/init.lua @@ -3,6 +3,9 @@ local M = {} -- layout exposed through here M.layout = require("dbee.utils.layout") +-- prompt for multiple parameters +M.prompt = require("dbee.utils.prompt") + -- Get random key from table ---@param tbl table key-value table ---@return any|nil key diff --git a/lua/dbee/prompt.lua b/lua/dbee/utils/prompt.lua similarity index 100% rename from lua/dbee/prompt.lua rename to lua/dbee/utils/prompt.lua From 9bc803104110e86101c1afc72fe13b6cc88a9d47 Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Tue, 16 May 2023 21:07:04 +0200 Subject: [PATCH 07/25] added loaders as additional config for handler --- lua/dbee.lua | 37 +++++++++++-------------- lua/dbee/config.lua | 24 ++++++++++++++-- lua/dbee/handler.lua | 66 ++++++++++++++++++-------------------------- 3 files changed, 64 insertions(+), 63 deletions(-) diff --git a/lua/dbee.lua b/lua/dbee.lua index be1e9028..4f500067 100644 --- a/lua/dbee.lua +++ b/lua/dbee.lua @@ -21,8 +21,21 @@ local function lazy_setup() -- add install binary to path vim.env.PATH = install.path() .. ":" .. vim.env.PATH + -- join loader.load and listed connections + local load = function() + local conns = m.config.connections or {} + if type(m.config.loader.load) == "function" then + local ok, loaded = pcall(m.config.loader.load) + if ok then + conns = vim.list_extend(conns, loaded) + end + end + return conns + end + local loader_config = { save = m.config.loader.save, load = load } + -- set up modules - m.handler = Handler:new(m.config.connections, m.config.result) + m.handler = Handler:new { result = m.config.result, loader = loader_config } m.editor = Editor:new(m.handler, m.config.editor) m.drawer = Drawer:new(m.handler, m.editor, m.config.drawer) @@ -53,6 +66,8 @@ function M.setup(o) -- validate config vim.validate { connections = { opts.connections, "table" }, + loader_load = { opts.loader.load, "function" }, + loader_save = { opts.loader.save, "function" }, lazy = { opts.lazy, "boolean" }, extra_helpers = { opts.extra_helpers, "table" }, -- submodules @@ -79,26 +94,6 @@ function M.setup(o) pcall_lazy_setup() end ----@param from "file"|"env" ----@param opt? string path to file or environment variable -function M.load_connections(from, opt) - if not pcall_lazy_setup() then - return - end - - local conns = {} - local loader = require("dbee.loader") - if from == "file" then - conns = loader.from_file(opt) - elseif from == "env" then - conns = loader.from_env(opt) - end - - for _, conn in ipairs(conns) do - m.handler:add_connection(utils.expand_environmet(conn) --[[@as connection_details]]) - end -end - ---@param connection connection_details function M.add_connection(connection) if not pcall_lazy_setup() then diff --git a/lua/dbee/config.lua b/lua/dbee/config.lua index c2d9d7c5..28c8d4b4 100644 --- a/lua/dbee/config.lua +++ b/lua/dbee/config.lua @@ -15,11 +15,12 @@ local m = {} -- configuration object ---@class Config ---@field connections connection_details[] list of configured database connections +---@field loader { save: fun(conns: connection_details[]), load: fun():connection_details[] } list of configured database connections ---@field extra_helpers table extra table helpers to provide besides built-ins. example: { postgres = { List = "select..." } ---@field lazy boolean lazy load the plugin or not? ---@field drawer drawer_config ---@field editor editor_config ----@field result handler_config +---@field result result_config ---@field ui UiConfig -- default configuration @@ -29,8 +30,7 @@ M.default = { -- lazy load the plugin or not? lazy = false, - -- list of connections - -- don't commit that, use something like nvim-projector for project specific config. + -- list of manually specified connections connections = { -- example: -- { @@ -38,6 +38,24 @@ M.default = { -- type = "postgres", -- url = "postgres://user:password@localhost:5432/db?sslmode=disable", -- }, + -- + }, + -- load/save functionality for connectoins + -- you can use defaults or come up with your own source + loader = { + -- you can control what happens with this function, + -- when the application wants to save connections - for example from "Add Connection" prompt + save = function(connections) + -- save to default file + require("dbee.loader").to_file(connections) + end, + -- use this function to provide different connections from files, env... + load = function() + -- load from default env var and file + local file_conns = require("dbee.loader").from_file() + local env_conns = require("dbee.loader").from_env() + return vim.tbl_deep_extend("keep", file_conns, env_conns) + end, }, -- extra table helpers per connection type extra_helpers = { diff --git a/lua/dbee/handler.lua b/lua/dbee/handler.lua index 77cf9e02..dbd36f3d 100644 --- a/lua/dbee/handler.lua +++ b/lua/dbee/handler.lua @@ -11,7 +11,8 @@ local utils = require("dbee.utils") ---@field database? string parent database ---@field children? Layout[] child layout nodes -- ----@alias handler_config { mappings: table, page_size: integer, window_command: string|fun():integer } +---@alias result_config { mappings: table, page_size: integer, window_command: string|fun():integer } +---@alias loader_config { save: fun(conns: connection_details[]), load: fun():connection_details[] } -- Handler is a wrapper around the go code -- it is the central part of the plugin and manages connections. @@ -25,50 +26,27 @@ local utils = require("dbee.utils") ---@field private win_cmd fun():integer function which opens a new window and returns a window id ---@field private page_size integer number of rows per page ---@field private mappings table +---@field private loader_save fun(conns: connection_details[]) function to save connections +---@field private loader_load fun():connection_details[] function to load connections local Handler = {} ----@param connections? connection_details[] ----@param opts? handler_config +---@param opts? { result: result_config, loader: loader_config } ---@return Handler -function Handler:new(connections, opts) - connections = connections or {} +function Handler:new(opts) opts = opts or {} + opts.result = opts.result or {} + opts.loader = opts.loader or {} - local page_size = opts.page_size or 100 - - local active = "ž" -- this MUST get overwritten - local conns = {} - for _, conn in ipairs(connections) do - if not conn.url then - error("url needs to be set!") - end - if not conn.type then - error("no type") - end - conn.type = utils.type_alias(conn.type) - - conn.name = conn.name or "[empty name]" - local id = "__master_connection_id_" .. conn.name .. conn.type .. "__" - - conn.id = id - if id < active then - active = id - end - - -- register in go - vim.fn.Dbee_register_connection(id, conn.url, conn.type, tostring(page_size)) - - conns[id] = conn - end + local page_size = opts.result.page_size or 100 local win_cmd - if type(opts.window_command) == "string" then + if type(opts.result.window_command) == "string" then win_cmd = function() - vim.cmd(opts.window_command) + vim.cmd(opts.result.window_command) return vim.api.nvim_get_current_win() end - elseif type(opts.window_command) == "function" then - win_cmd = opts.window_command + elseif type(opts.result.window_command) == "function" then + win_cmd = opts.result.window_command else win_cmd = function() vim.cmd("bo 15split") @@ -78,15 +56,24 @@ function Handler:new(connections, opts) -- class object local o = { - connections = conns, - active_connection = active, + connections = {}, + active_connection = "", page_index = 0, win_cmd = win_cmd, page_size = page_size, - mappings = opts.mappings or {}, + mappings = opts.result.mappings or {}, + loader_save = opts.loader.save or function() end, + loader_load = opts.loader.load or function() end, } setmetatable(o, self) self.__index = self + + -- initialize connections from loader + local conns = o.loader_load() + for _, conn in ipairs(conns) do + pcall(o.add_connection, o, conn) + end + return o end @@ -320,6 +307,7 @@ function Handler:layout() }) end + -- add connection dialog table.insert(layout, { id = "__add_connection__", name = "add connection", @@ -335,7 +323,7 @@ function Handler:layout() callback = function(result) local ok = pcall(self.add_connection, self, utils.expand_environmet(result) --[[@as connection_details]]) if ok then - require("dbee.loader").to_file { result } + self.loader_save { result } end cb() end, From 2c5304ac24d0590c5344cea0f848dc6991e74d57 Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Wed, 17 May 2023 18:55:23 +0200 Subject: [PATCH 08/25] added loader options to readme, fixed typo --- README.md | 126 +++++++++++++++++++++++++++++++++++++++- lua/dbee.lua | 2 +- lua/dbee/handler.lua | 4 +- lua/dbee/utils/init.lua | 2 +- 4 files changed, 129 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f5dba36e..dbc331dd 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ users. If that doesn't include you, then you have a few options: go build [-o ~/.local/share/nvim/dbee/bin/dbee] ``` -## Quick Start +## Usage Call the `setup()` function with an optional config parameter. If you are not using your plugin manager to lazy load for you, make sure to specify @@ -131,6 +131,130 @@ require("dbee").execute(query) require("dbee").save(format, file) ``` +### Specifying Connections + +Connection represents an instance of the database client (i.e. one database). +This is how it looks like: + +```lua +{ + name = "My Database", + type = "sqlite", -- type of database driver + url = "~/path/to/mydb.db", +} +``` + +There are a few different ways you can use to specify the connection parameters +for DBee: + +- Using the `setup()` function: + + The most straightforward (but probably the most useless) way is to just add + them to your configuration in `init.lua` like this: + + ```lua + require("dbee").setup { + connections = { + { + name = "My Database", + type = "sqlite", -- type of database driver + url = "~/path/to/mydb.db", + }, + -- ... + }, + -- ... the rest of your config + } + ``` + +- Use the prompt at runtime: + + You can add connections manually using the "add connection" item in the + drawer. Fill in the values and write the buffer (`:w`) to save the connection. + By default, this will save the connection to the global connections file and + will persist over restarts. + +- Use an env variable. This variable is `DBEE_CONNECTIONS` by default: + + You can export an environment variable with connections from your shell like + this: + + ```sh + export DBEE_CONNECTIONS='[ + { + "name": "DB from env", + "url": "mysql://...", + "type": "mysql" + } + ]' + ``` + +- Use a custom load function: + + If you aren't satisfied with the default capabilities, you can provide your + own `load` function in the config at setup. This example uses a + project-specific connections config file: + + ```lua + local file = vim.fn.getcwd() .. "/.dbee.json" + + require("dbee").setup { + loader = { + -- this function must return a list of connections and it doesn't + -- care about anything else + load = function() + return require("dbee.loader").from_file(file) + end, + + -- just as an example you can also specify this function to save any + -- connections from the prompt input to the same file + save = function(connections) + require("dbee.loader").to_file(file) + end, + }, + -- ... the rest of your config + } + ``` + +#### Secrets + +If you don't want to have secrets laying around your disk in plain text, you can +use the special placeholders in connection strings (this works using any method +for specifying connections). + +NOTE: *Currently only envirnoment variables are supported* + +Example: + +Using the `DBEE_CONNECTIONS` environment variable for specifying connections and +exporting secrets to environment: + +```sh +# Define connections +export DBEE_CONNECTIONS='[ + { + "name": "{{ env.SECRET_DB_NAME }}", + "url": "postgres://{{ env.SECRET_DB_USER }}:{{ env.SECRET_DB_PASS }}@localhost:5432/{{ env.SECRET_DB_NAME }}?sslmode=disable", + "type": "postgres" + } +]' + +# Export secrets +export SECRET_DB_NAME="secretdb" +export SECRET_DB_USER="secretuser" +export SECRET_DB_PASS="secretpass" +``` + +If you start neovim in the same shell, this will evaluate to the following +connection: + +```lua +{ { + name = "secretdb", + url = "postgres://secretuser:secretpass@localhost:5432/secretdb?sslmode=disable", + type = "postgres", +} } +``` + ## Configuration As mentioned, you can pass an optional table parameter to `setup()` function. diff --git a/lua/dbee.lua b/lua/dbee.lua index 4f500067..eb67a6bb 100644 --- a/lua/dbee.lua +++ b/lua/dbee.lua @@ -99,7 +99,7 @@ function M.add_connection(connection) if not pcall_lazy_setup() then return end - m.handler:add_connection(utils.expand_environmet(connection) --[[@as connection_details]]) + m.handler:add_connection(utils.expand_environment(connection) --[[@as connection_details]]) end function M.open() diff --git a/lua/dbee/handler.lua b/lua/dbee/handler.lua index dbd36f3d..378a8aea 100644 --- a/lua/dbee/handler.lua +++ b/lua/dbee/handler.lua @@ -71,7 +71,7 @@ function Handler:new(opts) -- initialize connections from loader local conns = o.loader_load() for _, conn in ipairs(conns) do - pcall(o.add_connection, o, conn) + pcall(o.add_connection, o, utils.expand_environment(conn)) end return o @@ -321,7 +321,7 @@ function Handler:layout() utils.prompt.open(prompt, { title = "Add Connection", callback = function(result) - local ok = pcall(self.add_connection, self, utils.expand_environmet(result) --[[@as connection_details]]) + local ok = pcall(self.add_connection, self, utils.expand_environment(result) --[[@as connection_details]]) if ok then self.loader_save { result } end diff --git a/lua/dbee/utils/init.lua b/lua/dbee/utils/init.lua index e5018750..48a43878 100644 --- a/lua/dbee/utils/init.lua +++ b/lua/dbee/utils/init.lua @@ -78,7 +78,7 @@ end -- Replaces {{ env.SOMETHING }} with environment or empty string ---@param obj string|table ---@return string|table -function M.expand_environmet(obj) +function M.expand_environment(obj) local function expand(o) if type(o) ~= "string" then return o From 449f239b7e99e9ef0e39dc828957e141dc5a7f95 Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Wed, 17 May 2023 19:49:52 +0200 Subject: [PATCH 09/25] - added remove connection functionality - renamed loader functions - updated readme and config --- README.md | 15 ++++++++++----- lua/dbee.lua | 5 +++-- lua/dbee/config.lua | 29 ++++++++++++++++++---------- lua/dbee/handler.lua | 45 +++++++++++++++++++++++++++++++++++++------- lua/dbee/loader.lua | 44 ++++++++++++++++++++++++++++++++++++------- 5 files changed, 107 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index dbc331dd..b40f0569 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,8 @@ This is how it looks like: ```lua { + id = "optional_identifier" -- useful to set manually if you want to remove from the file (see below) + -- IT'S YOUR JOB TO KEEP THESE UNIQUE! name = "My Database", type = "sqlite", -- type of database driver url = "~/path/to/mydb.db", @@ -202,13 +204,16 @@ for DBee: -- this function must return a list of connections and it doesn't -- care about anything else load = function() - return require("dbee.loader").from_file(file) + return require("dbee.loader").load_from_file(file) end, - -- just as an example you can also specify this function to save any - -- connections from the prompt input to the same file - save = function(connections) - require("dbee.loader").to_file(file) + -- connections from the prompt input to the same file as they are being loaded from + add = function(connections) + require("dbee.loader").add_to_file(file) + end, + -- and this to remove them + remove = function(connections) + require("dbee.loader").remove_from_file(file) end, }, -- ... the rest of your config diff --git a/lua/dbee.lua b/lua/dbee.lua index eb67a6bb..ac0514d7 100644 --- a/lua/dbee.lua +++ b/lua/dbee.lua @@ -32,7 +32,7 @@ local function lazy_setup() end return conns end - local loader_config = { save = m.config.loader.save, load = load } + local loader_config = { add = m.config.loader.add, remove = m.config.loader.remove, load = load } -- set up modules m.handler = Handler:new { result = m.config.result, loader = loader_config } @@ -67,7 +67,8 @@ function M.setup(o) vim.validate { connections = { opts.connections, "table" }, loader_load = { opts.loader.load, "function" }, - loader_save = { opts.loader.save, "function" }, + loader_save = { opts.loader.add, "function" }, + loader_remove = { opts.loader.remove, "function" }, lazy = { opts.lazy, "boolean" }, extra_helpers = { opts.extra_helpers, "table" }, -- submodules diff --git a/lua/dbee/config.lua b/lua/dbee/config.lua index 28c8d4b4..67529f39 100644 --- a/lua/dbee/config.lua +++ b/lua/dbee/config.lua @@ -15,12 +15,12 @@ local m = {} -- configuration object ---@class Config ---@field connections connection_details[] list of configured database connections ----@field loader { save: fun(conns: connection_details[]), load: fun():connection_details[] } list of configured database connections ---@field extra_helpers table extra table helpers to provide besides built-ins. example: { postgres = { List = "select..." } ---@field lazy boolean lazy load the plugin or not? ---@field drawer drawer_config ---@field editor editor_config ---@field result result_config +---@field loader loader_config ---@field ui UiConfig -- default configuration @@ -38,23 +38,32 @@ M.default = { -- type = "postgres", -- url = "postgres://user:password@localhost:5432/db?sslmode=disable", -- }, - -- }, -- load/save functionality for connectoins - -- you can use defaults or come up with your own source + -- you can use helper function from require("dbee.loader") + -- or come up with something completly different loader = { -- you can control what happens with this function, -- when the application wants to save connections - for example from "Add Connection" prompt - save = function(connections) - -- save to default file - require("dbee.loader").to_file(connections) + -- recieves a list of connections + add = function(connections) + -- append to default file + require("dbee.loader").add_to_file(connections) + end, + -- you can control what happens with this function, + -- when the application wants to remove connections - for example from drawer action + -- recieves a list of connections + remove = function(connections) + -- remove from default file + require("dbee.loader").remove_from_file(connections) end, -- use this function to provide different connections from files, env... + -- must return a list of connections load = function() -- load from default env var and file - local file_conns = require("dbee.loader").from_file() - local env_conns = require("dbee.loader").from_env() - return vim.tbl_deep_extend("keep", file_conns, env_conns) + local file_conns = require("dbee.loader").load_from_file() + local env_conns = require("dbee.loader").load_from_env() + return vim.list_extend(file_conns, env_conns) end, }, -- extra table helpers per connection type @@ -79,7 +88,7 @@ M.default = { action_1 = { key = "", mode = "n" }, -- action_2 renames a scratchpad or sets the connection as active manually action_2 = { key = "da", mode = "n" }, - -- action_3 deletes a scratchpad + -- action_3 deletes a scratchpad or connection (removes connection from the file if you configured it like so) action_3 = { key = "dd", mode = "n" }, -- these are self-explanatory: collapse = { key = "c", mode = "n" }, diff --git a/lua/dbee/handler.lua b/lua/dbee/handler.lua index 378a8aea..51700c86 100644 --- a/lua/dbee/handler.lua +++ b/lua/dbee/handler.lua @@ -12,7 +12,7 @@ local utils = require("dbee.utils") ---@field children? Layout[] child layout nodes -- ---@alias result_config { mappings: table, page_size: integer, window_command: string|fun():integer } ----@alias loader_config { save: fun(conns: connection_details[]), load: fun():connection_details[] } +---@alias loader_config { add: fun(conns: connection_details[]), remove: fun(conns: connection_details[]), load: fun():connection_details[] } -- Handler is a wrapper around the go code -- it is the central part of the plugin and manages connections. @@ -26,8 +26,9 @@ local utils = require("dbee.utils") ---@field private win_cmd fun():integer function which opens a new window and returns a window id ---@field private page_size integer number of rows per page ---@field private mappings table ----@field private loader_save fun(conns: connection_details[]) function to save connections ----@field private loader_load fun():connection_details[] function to load connections +---@field private loader_add fun(conns: connection_details[]) function to add connections to external source +---@field private loader_remove fun(conns: connection_details[]) function to remove connections from external source +---@field private loader_load fun():connection_details[] function to load connections from external source local Handler = {} ---@param opts? { result: result_config, loader: loader_config } @@ -62,7 +63,8 @@ function Handler:new(opts) win_cmd = win_cmd, page_size = page_size, mappings = opts.result.mappings or {}, - loader_save = opts.loader.save or function() end, + loader_add = opts.loader.add or function() end, + loader_remove = opts.loader.remove or function() end, loader_load = opts.loader.load or function() end, } setmetatable(o, self) @@ -78,6 +80,7 @@ function Handler:new(opts) end ---@param connection connection_details +---@return conn_id # id of the added connection function Handler:add_connection(connection) if not connection.url then error("url needs to be set!") @@ -88,7 +91,7 @@ function Handler:add_connection(connection) connection.name = connection.name or "[empty name]" connection.type = utils.type_alias(connection.type) - connection.id = "__master_connection_id_" .. connection.name .. connection.type .. "__" + connection.id = connection.id or ("__master_connection_id_" .. connection.name .. connection.type .. "__") -- register in go local ok = vim.fn.Dbee_register_connection(connection.id, connection.url, connection.type, tostring(self.page_size)) @@ -98,6 +101,21 @@ function Handler:add_connection(connection) self.connections[connection.id] = connection self.active_connection = connection.id + + return connection.id +end + +-- removes/unregisters connection +---@param id conn_id connection id +function Handler:remove_connection(id) + if not id then + return + end + + self.connections[id] = nil + if self.active_connection == id then + self.active_connection = utils.random_key(self.connections) + end end ---@param id conn_id connection id @@ -301,6 +319,17 @@ function Handler:layout() self:set_active(conn.id) cb() end, + -- remove connection (also trigger the loader function) + action_3 = function(cb) + vim.ui.input({ prompt = 'confirm deletion of "' .. conn.name .. '"', default = "Y" }, function(input) + if not input or string.lower(input) ~= "y" then + return + end + self:remove_connection(conn.id) + self.loader_remove { conn } + cb() + end) + end, children = function() return self:get_connection_layout(conn.id) end, @@ -321,9 +350,11 @@ function Handler:layout() utils.prompt.open(prompt, { title = "Add Connection", callback = function(result) - local ok = pcall(self.add_connection, self, utils.expand_environment(result) --[[@as connection_details]]) + local ok, added_id = + pcall(self.add_connection, self, utils.expand_environment(result) --[[@as connection_details]]) if ok then - self.loader_save { result } + result.id = added_id + self.loader_add { result } end cb() end, diff --git a/lua/dbee/loader.lua b/lua/dbee/loader.lua index 142969ff..b23de632 100644 --- a/lua/dbee/loader.lua +++ b/lua/dbee/loader.lua @@ -7,7 +7,7 @@ local M = {} -- Parses json file with connections ---@param path? string path to file ---@return connection_details[] -function M.from_file(path) +function M.load_from_file(path) path = path or DEFAULT_PERSISTENCE_FILE ---@type connection_details[] @@ -43,7 +43,7 @@ end -- Parses env variable if it exists ---@param var? string env var to check - default: DBEE_CONNECTIONS ---@return connection_details[] -function M.from_env(var) +function M.load_from_env(var) var = var or "DBEE_CONNECTIONS" ---@type connection_details[] @@ -69,20 +69,50 @@ function M.from_env(var) return conns end --- saves connection_details to a file +-- appends connection_details to a json ---@param connections connection_details[] ---@param path? string path to save file -function M.to_file(connections, path) +function M.add_to_file(connections, path) path = path or DEFAULT_PERSISTENCE_FILE if not connections or vim.tbl_isempty(connections) then return end - local existing = M.from_file(path) + local existing = M.load_from_file(path) - for _, conn in ipairs(connections) do - table.insert(existing, conn) + existing = vim.list_extend(existing, connections) + + local ok, json = pcall(vim.fn.json_encode, existing) + if not ok then + utils.log("error", "Could not convert connection list to json", "loader") + return + end + + -- overwrite file + local file = assert(io.open(path, "w+"), "could not open file") + file:write(json) + file:close() +end + +-- removes connection_details from a json file +---@param connections connection_details[] +---@param path? string path to save file +function M.remove_from_file(connections, path) + path = path or DEFAULT_PERSISTENCE_FILE + + if not connections or vim.tbl_isempty(connections) then + return + end + + local existing = M.load_from_file(path) + + for _, to_remove in ipairs(connections) do + for i, ex_conn in ipairs(existing) do + if to_remove.id == ex_conn.id then + table.remove(existing, i) + end + end end local ok, json = pcall(vim.fn.json_encode, existing) From 8aa4279954a183c9bb8b313f10a427df56e52bfc Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 17 May 2023 17:51:15 +0000 Subject: [PATCH 10/25] [docgen] Update doc/dbee.txt --- doc/dbee.txt | 171 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 166 insertions(+), 5 deletions(-) diff --git a/doc/dbee.txt b/doc/dbee.txt index 87ca4d1b..060378f3 100644 --- a/doc/dbee.txt +++ b/doc/dbee.txt @@ -8,7 +8,9 @@ CONTENTS *dbee-content 1.1. Installation..........................................|dbee-installation| 1.1.1. Platform Support............................|dbee-platform_support| 1.1.2. Manual Binary Installation........|dbee-manual_binary_installation| - 1.2. Quick Start............................................|dbee-quick_start| + 1.2. Usage........................................................|dbee-usage| + 1.2.1. Specifying Connections................|dbee-specifying_connections| + 1.2.1.1. Secrets........................................|dbee-secrets| 1.3. Configuration........................................|dbee-configuration| 1.4. Projector Integration........................|dbee-projector_integration| 1.5. Development............................................|dbee-development| @@ -118,7 +120,7 @@ users. If that doesn't include you, then you have a few options: < -------------------------------------------------------------------------------- -QUICK START *dbee-quick_start* +USAGE *dbee-usage* Call the `setup()` function with an optional config parameter. If you are not using your plugin manager to lazy load for you, make sure to specify @@ -139,6 +141,127 @@ Here is a brief refference of the most useful functions: require("dbee").save(format, file) < +SPECIFYING CONNECTIONS *dbee-specifying_connections* + +Connection represents an instance of the database client (i.e. one database). +This is how it looks like: +> + { + id = "optional_identifier" -- useful to set manually if you want to remove from the file (see below) + -- IT'S YOUR JOB TO KEEP THESE UNIQUE! + name = "My Database", + type = "sqlite", -- type of database driver + url = "~/path/to/mydb.db", + } +< + +There are a few different ways you can use to specify the connection parameters +for DBee: + +* Using the `setup()` function: + +The most straightforward (but probably the most useless) way is to just add + them to your configuration in `init.lua` like this: +> + require("dbee").setup { + connections = { + { + name = "My Database", + type = "sqlite", -- type of database driver + url = "~/path/to/mydb.db", + }, + -- ... + }, + -- ... the rest of your config + } +< + +* Use the prompt at runtime: + +You can add connections manually using the "add connection" item in the + drawer. Fill in the values and write the buffer (`:w`) to save the connection. + By default, this will save the connection to the global connections file and + will persist over restarts. + +* Use an env variable. This variable is `DBEE_CONNECTIONS` by default: + +You can export an environment variable with connections from your shell like + this: +> + export DBEE_CONNECTIONS='[ + { + "name": "DB from env", + "url": "mysql://...", + "type": "mysql" + } + ]' +< + +* Use a custom load function: + +If you aren't satisfied with the default capabilities, you can provide your + own `load` function in the config at setup. This example uses a + project-specific connections config file: +> + local file = vim.fn.getcwd() .. "/.dbee.json" + require("dbee").setup { + loader = { + -- this function must return a list of connections and it doesn't + -- care about anything else + load = function() + return require("dbee.loader").load_from_file(file) + end, + -- just as an example you can also specify this function to save any + -- connections from the prompt input to the same file as they are being loaded from + add = function(connections) + require("dbee.loader").add_to_file(file) + end, + -- and this to remove them + remove = function(connections) + require("dbee.loader").remove_from_file(file) + end, + }, + -- ... the rest of your config + } +< + +SECRETS *dbee-secrets* + +If you don't want to have secrets laying around your disk in plain text, you can +use the special placeholders in connection strings (this works using any method +for specifying connections). + +NOTE: Currently only envirnoment variables are supported + +Example: + +Using the `DBEE_CONNECTIONS` environment variable for specifying connections and +exporting secrets to environment: +> + # Define connections + export DBEE_CONNECTIONS='[ + { + "name": "{{ env.SECRET_DB_NAME }}", + "url": "postgres://{{ env.SECRET_DB_USER }}:{{ env.SECRET_DB_PASS }}@localhost:5432/{{ env.SECRET_DB_NAME }}?sslmode=disable", + "type": "postgres" + } + ]' + # Export secrets + export SECRET_DB_NAME="secretdb" + export SECRET_DB_USER="secretuser" + export SECRET_DB_PASS="secretpass" +< + +If you start neovim in the same shell, this will evaluate to the following +connection: +> + { { + name = "secretdb", + url = "postgres://secretuser:secretpass@localhost:5432/secretdb?sslmode=disable", + type = "postgres", + } } +< + -------------------------------------------------------------------------------- CONFIGURATION *dbee-configuration* @@ -149,8 +272,7 @@ Here are the defaults: M.default = { -- lazy load the plugin or not? lazy = false, - -- list of connections - -- don't commit that, use something like nvim-projector for project specific config. + -- list of manually specified connections connections = { -- example: -- { @@ -159,6 +281,33 @@ Here are the defaults: -- url = "postgres://user:password@localhost:5432/db?sslmode=disable", -- }, }, + -- load/save functionality for connectoins + -- you can use helper function from require("dbee.loader") + -- or come up with something completly different + loader = { + -- you can control what happens with this function, + -- when the application wants to save connections - for example from "Add Connection" prompt + -- recieves a list of connections + add = function(connections) + -- append to default file + require("dbee.loader").add_to_file(connections) + end, + -- you can control what happens with this function, + -- when the application wants to remove connections - for example from drawer action + -- recieves a list of connections + remove = function(connections) + -- remove from default file + require("dbee.loader").remove_from_file(connections) + end, + -- use this function to provide different connections from files, env... + -- must return a list of connections + load = function() + -- load from default env var and file + local file_conns = require("dbee.loader").load_from_file() + local env_conns = require("dbee.loader").load_from_env() + return vim.list_extend(file_conns, env_conns) + end, + }, -- extra table helpers per connection type extra_helpers = { -- example: @@ -180,7 +329,7 @@ Here are the defaults: action_1 = { key = "", mode = "n" }, -- action_2 renames a scratchpad or sets the connection as active manually action_2 = { key = "da", mode = "n" }, - -- action_3 deletes a scratchpad + -- action_3 deletes a scratchpad or connection (removes connection from the file if you configured it like so) action_3 = { key = "dd", mode = "n" }, -- these are self-explanatory: collapse = { key = "c", mode = "n" }, @@ -207,6 +356,18 @@ Here are the defaults: icon = "", highlight = "Conditional", }, + add = { + icon = "", + highlight = "String", + }, + remove = { + icon = "󰆴", + highlight = "SpellBad", + }, + help = { + icon = "󰋖", + highlight = "NormalFloat", + }, -- if there is no type -- use this for normal nodes... none = { From 68fff7bb10e3d4fffcd60f9d3b6d8ff693aec94e Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 17 May 2023 18:00:35 +0000 Subject: [PATCH 11/25] [install] Update lua/dbee/install/__manifest.lua --- lua/dbee/install/__manifest.lua | 42 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lua/dbee/install/__manifest.lua b/lua/dbee/install/__manifest.lua index ab6affbd..0595b32c 100644 --- a/lua/dbee/install/__manifest.lua +++ b/lua/dbee/install/__manifest.lua @@ -4,29 +4,29 @@ local M = {} -- Links to binary releases M.urls = { - dbee_android_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_android_amd64", - dbee_android_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_android_arm64", - dbee_darwin_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_darwin_amd64", - dbee_darwin_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_darwin_arm64", - dbee_freebsd_386 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_freebsd_386", - dbee_freebsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_freebsd_amd64", - dbee_freebsd_arm = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_freebsd_arm", - dbee_freebsd_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_freebsd_arm64", - dbee_linux_386 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_linux_386", - dbee_linux_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_linux_amd64", - dbee_linux_arm = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_linux_arm", - dbee_linux_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_linux_arm64", - dbee_linux_ppc64le = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_linux_ppc64le", - dbee_linux_riscv64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_linux_riscv64", - dbee_linux_s390x = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_linux_s390x", - dbee_netbsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_netbsd_amd64", - dbee_openbsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_openbsd_amd64", - dbee_openbsd_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_openbsd_arm64", - dbee_windows_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_windows_amd64", - dbee_windows_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/58b7bfdd391ffdcaf89724b8d70674d7903c4b43/artifacts/dbee_windows_arm64", + dbee_android_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_android_amd64", + dbee_android_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_android_arm64", + dbee_darwin_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_darwin_amd64", + dbee_darwin_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_darwin_arm64", + dbee_freebsd_386 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_freebsd_386", + dbee_freebsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_freebsd_amd64", + dbee_freebsd_arm = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_freebsd_arm", + dbee_freebsd_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_freebsd_arm64", + dbee_linux_386 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_linux_386", + dbee_linux_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_linux_amd64", + dbee_linux_arm = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_linux_arm", + dbee_linux_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_linux_arm64", + dbee_linux_ppc64le = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_linux_ppc64le", + dbee_linux_riscv64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_linux_riscv64", + dbee_linux_s390x = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_linux_s390x", + dbee_netbsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_netbsd_amd64", + dbee_openbsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_openbsd_amd64", + dbee_openbsd_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_openbsd_arm64", + dbee_windows_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_windows_amd64", + dbee_windows_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_windows_arm64", } -- Current version of go main package -M.version = "95cd5a3190d80f9c4337ad77e76db9d77bbc4872" +M.version = "449f239b7e99e9ef0e39dc828957e141dc5a7f95" return M From fa4cda76d6f356870018ebfe0f3bd64b62dd9282 Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Wed, 17 May 2023 20:03:15 +0200 Subject: [PATCH 12/25] fix lua style --- lua/dbee/editor.lua | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lua/dbee/editor.lua b/lua/dbee/editor.lua index a9d162df..882411d0 100644 --- a/lua/dbee/editor.lua +++ b/lua/dbee/editor.lua @@ -201,12 +201,14 @@ function Editor:layout() table.insert(scratches, sch) end - return { { - id = "__master_scratchpad__", - name = "scratchpads", - type = "scratch", - children = scratches, - } } + return { + { + id = "__master_scratchpad__", + name = "scratchpads", + type = "scratch", + children = scratches, + }, + } end ---@param id scratch_id scratch id - name From 23269bc1834abf088b95ab07da770689ee895416 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 17 May 2023 18:12:43 +0000 Subject: [PATCH 13/25] [install] Update lua/dbee/install/__manifest.lua --- lua/dbee/install/__manifest.lua | 42 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lua/dbee/install/__manifest.lua b/lua/dbee/install/__manifest.lua index 0595b32c..cefcc5f1 100644 --- a/lua/dbee/install/__manifest.lua +++ b/lua/dbee/install/__manifest.lua @@ -4,29 +4,29 @@ local M = {} -- Links to binary releases M.urls = { - dbee_android_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_android_amd64", - dbee_android_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_android_arm64", - dbee_darwin_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_darwin_amd64", - dbee_darwin_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_darwin_arm64", - dbee_freebsd_386 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_freebsd_386", - dbee_freebsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_freebsd_amd64", - dbee_freebsd_arm = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_freebsd_arm", - dbee_freebsd_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_freebsd_arm64", - dbee_linux_386 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_linux_386", - dbee_linux_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_linux_amd64", - dbee_linux_arm = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_linux_arm", - dbee_linux_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_linux_arm64", - dbee_linux_ppc64le = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_linux_ppc64le", - dbee_linux_riscv64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_linux_riscv64", - dbee_linux_s390x = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_linux_s390x", - dbee_netbsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_netbsd_amd64", - dbee_openbsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_openbsd_amd64", - dbee_openbsd_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_openbsd_arm64", - dbee_windows_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_windows_amd64", - dbee_windows_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/1cd5065487d2447169d364dffb10d6a20c9e1dac/artifacts/dbee_windows_arm64", + dbee_android_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_android_amd64", + dbee_android_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_android_arm64", + dbee_darwin_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_darwin_amd64", + dbee_darwin_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_darwin_arm64", + dbee_freebsd_386 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_freebsd_386", + dbee_freebsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_freebsd_amd64", + dbee_freebsd_arm = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_freebsd_arm", + dbee_freebsd_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_freebsd_arm64", + dbee_linux_386 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_linux_386", + dbee_linux_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_linux_amd64", + dbee_linux_arm = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_linux_arm", + dbee_linux_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_linux_arm64", + dbee_linux_ppc64le = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_linux_ppc64le", + dbee_linux_riscv64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_linux_riscv64", + dbee_linux_s390x = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_linux_s390x", + dbee_netbsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_netbsd_amd64", + dbee_openbsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_openbsd_amd64", + dbee_openbsd_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_openbsd_arm64", + dbee_windows_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_windows_amd64", + dbee_windows_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_windows_arm64", } -- Current version of go main package -M.version = "449f239b7e99e9ef0e39dc828957e141dc5a7f95" +M.version = "fa4cda76d6f356870018ebfe0f3bd64b62dd9282" return M From 26e213666e5a6661ef85c8fd75f81254240a3b53 Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Wed, 17 May 2023 21:28:53 +0200 Subject: [PATCH 14/25] added getting started to readme and added folding sections --- .github/workflows/docgen.yml | 7 +- ARCHITECTURE.md | 12 +-- README.md | 143 ++++++++++++++++++++++++++++++----- 3 files changed, 137 insertions(+), 25 deletions(-) diff --git a/.github/workflows/docgen.yml b/.github/workflows/docgen.yml index 82eca907..bcad2a10 100644 --- a/.github/workflows/docgen.yml +++ b/.github/workflows/docgen.yml @@ -33,19 +33,20 @@ jobs: run: | TEMP_CONFIG="$(mktemp)" TEMP_README="$(mktemp)" - # Retrieve default config and put it in a temp file + # Retrieve default config and put it in a temp file. { echo '```lua' awk '/DOCGEN_END/{f=0} f; /DOCGEN_START/{f=1}' lua/dbee/config.lua echo '```' } > "$TEMP_CONFIG" - # Insert the default config between DOCGEN_CONFIG tags in the README + # Insert the default config between DOCGEN_CONFIG tags in the README. + # And remove stuff between DOCGEN_IGNORE_START and DOCGEN_IGNORE_END tags from README. { sed -e ' /DOCGEN_CONFIG_START/,/DOCGEN_CONFIG_END/!b /DOCGEN_CONFIG_START/r '"$TEMP_CONFIG"' /DOCGEN_CONFIG_END:/!d - ' <(sed '0,/DOCGEN_START/d' README.md) + ' <(sed '/DOCGEN_IGNORE_START/,/DOCGEN_IGNORE_END/d' README.md) cat ARCHITECTURE.md } > "$TEMP_README" # Generate docs diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b7e88838..a54d7580 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -28,11 +28,11 @@ responsibilities. │ │ │ - │ ┌─────────────┐ - │ │ │ - │ │ install │ - │ │ │ - │ └─────────────┘ + │ ┌─────────────┐ ┌────────────┐ + │ │ │ │ │ + │ │ install │ │ loader │ + │ │ │ │ │ + │ └─────────────┘ └────────────┘ │ ``` @@ -43,6 +43,8 @@ Description: - `install` package is independent of the other packages and is used for installation of the compiled go binary using the manifest generated by the CI pipeline. +- `loader` package is also independent and is used as the default loading and + saving method in the config, which is later consumed by the handler - `drawer` is the "tree" view in UI - it consumes the editor (to provide scratchpad view and to manage scratchpads) and the handler (for managing connections and providing layout of each database). diff --git a/README.md b/README.md index b40f0569..fe14493f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ - + + + ![Linting Status](https://img.shields.io/github/actions/workflow/status/kndndrj/nvim-dbee/lint.yml?label=linting&style=for-the-badge) ![Docgen Status](https://img.shields.io/github/actions/workflow/status/kndndrj/nvim-dbee/docgen.yml?label=docgen&logo=neovim&logoColor=white&style=for-the-badge) ![Backend](https://img.shields.io/badge/go-backend-lightblue?style=for-the-badge&logo=go&logoColor=white) ![Frontend](https://img.shields.io/badge/lua-frontend-blue?style=for-the-badge&logo=lua&logoColor=white) - + # Neovim DBee @@ -69,6 +71,12 @@ ### Platform Support + + +
+ Click to expand + + This project aims to be as cross-platform as possible, but there are some limitations (for example some of the go dependencies only work on certain platforms). @@ -82,8 +90,19 @@ created. So to check if your platform is currently supported, check out the mentioned manifest + + +
+ + ### Manual Binary Installation + + +
+ Click to expand + + The installation examples include the `build`/`run` functions, which get triggered once the plugin updates. This should be sufficient for the majority of users. If that doesn't include you, then you have a few options: @@ -109,13 +128,36 @@ users. If that doesn't include you, then you have a few options: go build [-o ~/.local/share/nvim/dbee/bin/dbee] ``` + + +
+ + +## Configuration + +You can pass an optional table parameter to `setup()` function. + +Here are the defaults: + + + + + +[`config.lua`](lua/dbee/config.lua) + + + ## Usage Call the `setup()` function with an optional config parameter. If you are not using your plugin manager to lazy load for you, make sure to specify `{ lazy = true }` in the config. -Here is a brief refference of the most useful functions: + + +
+ Brief reference (click to expand): + ```lua -- Open/close the UI. @@ -131,6 +173,83 @@ require("dbee").execute(query) require("dbee").save(format, file) ``` + + +
+ + +### Getting Started + +Here are a few steps to quickly get started: + +- call the `setup()` function in your `init.lua` + +- Specify connections using one or more sources (reffer to + [this section](#specifying-connections)). + +- When you restart the editor, call `lua require("dbee").open()` to open the UI. + +- Navigate to the drawer (tree) and use the following key-bindings to perform + different actions depending on the context (the mappings can all be changed in + the config): + + - All nodes: + + - Press `o` to toggle the tree node, `e` to expand and `c` to close. + - Press `r` to manually refresh the tree. + + - Connections: + + - Press `da` to set it as the active one. + - Press `dd` to delete it (this also triggers the `remove` function - see + more below.) + - Press `` to perform an action - view history or look at helper + queries. + + - Scratchpads: + + - Press `` on the `new` node to create a new scratchpad. + - When you try to save it to disk (`:w`), the path is automatically filled + for you. You can change the name to anything you want, if you save it to + the suggested directory, it will load the next time you open DBee. + - Press `da` to rename the scratchpad. + - Press `dd` to delete it (also from disk). + - Pressing `` on an existing scratchpad in the drawer will open it in + the editor pane. + + - Help: + + - Just view the key bindings. + +- Once you selected the connection and created a scratchpad, you can navigate to + the editor pane (top-right by default) and start writing queries. In editor + pane, you can use the following actions: + + - Highlight some text in visual mode and press `BB` - this will run the + selected query on the active connection. + - If you press `BB` in normal mode, you run the whole scratchpad on the active + connection. + +- If the request was successful, the results should appear in the "result" + buffer (bottom one by default). If the total number of results was lower than + the `page_size` parameter in config (100 by default), all results should + already be present. If there are more than `page_size` results, you can "page" + thrugh them using one of the following: + + - Using `require("dbee").next()` and `require("dbee").prev()` from anywhere + (even if your cursor is outside the result buffer). + - Using `L` for next and `H` for previous page if the cursor is located inside + the results buffer. + +- The current result (of the active connection) can also be saved to a file + using `require("dbee").save()` command. Use: + + - `require("dbee").save("csv", "/path/to/file.csv")` for csv and + - `require("dbee").save("json", "/path/to/file.json")` for json. + +- Once you are done or you want to go back to where you were, you can call + `require("dbee").close()`. + ### Specifying Connections Connection represents an instance of the database client (i.e. one database). @@ -260,20 +379,6 @@ connection: } } ``` -## Configuration - -As mentioned, you can pass an optional table parameter to `setup()` function. - -Here are the defaults: - - - - - -[`config.lua`](lua/dbee/config.lua) - - - ## Projector Integration DBee is compatible with my other plugin @@ -282,7 +387,11 @@ code-runner/project-configurator. To use dbee with it, simply use `"dbee"` as one of it's outputs. + + ## Development Reffer to [ARCHITECTURE.md](ARCHITECTURE.md) for a brief overview of the architecture. + + From b1020abae0e502316ab09ccef3d974fbabfe6fce Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Sat, 20 May 2023 11:32:44 +0200 Subject: [PATCH 15/25] - pulled UIs out of objects - pulled result out of handler - connections are maps to go side - added handler lookup for connections and loaders - handler just manages connections and loaders through lookup --- lua/dbee.lua | 89 ++++++--- lua/dbee/config.lua | 64 +++---- lua/dbee/conn.lua | 257 +++++++++++++++++++++++++ lua/dbee/drawer.lua | 75 ++------ lua/dbee/editor.lua | 56 ++---- lua/dbee/handler.lua | 446 ++++++++++--------------------------------- lua/dbee/loader.lua | 191 ++++++++++++------ lua/dbee/lookup.lua | 185 ++++++++++++++++++ lua/dbee/result.lua | 83 ++++++++ lua/dbee/ui.lua | 84 ++++++++ 10 files changed, 950 insertions(+), 580 deletions(-) create mode 100644 lua/dbee/conn.lua create mode 100644 lua/dbee/lookup.lua create mode 100644 lua/dbee/result.lua create mode 100644 lua/dbee/ui.lua diff --git a/lua/dbee.lua b/lua/dbee.lua index ac0514d7..85b88239 100644 --- a/lua/dbee.lua +++ b/lua/dbee.lua @@ -1,6 +1,11 @@ local Drawer = require("dbee.drawer") local Editor = require("dbee.editor") +local Result = require("dbee.result") +local Ui = require("dbee.ui") local Handler = require("dbee.handler") +local MemoryLoader = require("dbee.loader").MemoryLoader +local EnvLoader = require("dbee.loader").EnvLoader +local FileLoader = require("dbee.loader").FileLoader local install = require("dbee.install") local utils = require("dbee.utils") local default_config = require("dbee.config").default @@ -21,23 +26,52 @@ local function lazy_setup() -- add install binary to path vim.env.PATH = install.path() .. ":" .. vim.env.PATH - -- join loader.load and listed connections - local load = function() - local conns = m.config.connections or {} - if type(m.config.loader.load) == "function" then - local ok, loaded = pcall(m.config.loader.load) - if ok then - conns = vim.list_extend(conns, loaded) - end - end - return conns + -- set up UIs + local result_ui = Ui:new { + window_command = m.config.ui.window_commands.result, + window_options = { + wrap = false, + winfixheight = true, + winfixwidth = true, + number = false, + }, + } + local editor_ui = Ui:new { + window_command = m.config.ui.window_commands.editor, + } + local drawer_ui = Ui:new { + window_command = m.config.ui.window_commands.drawer, + buffer_options = { + buflisted = false, + bufhidden = "delete", + buftype = "nofile", + swapfile = false, + }, + window_options = { + wrap = false, + winfixheight = true, + winfixwidth = true, + number = false, + }, + } + + -- handler and loaders + -- memory loader loads configs from setup() and is also a default loader + local mem_loader = MemoryLoader:new(m.config.connections) + + local loaders = {} + for _, file in ipairs(m.config.connection_sources.files) do + table.insert(loaders, FileLoader:new(file)) + end + for _, var in ipairs(m.config.connection_sources.env_vars) do + table.insert(loaders, EnvLoader:new(var)) end - local loader_config = { add = m.config.loader.add, remove = m.config.loader.remove, load = load } -- set up modules - m.handler = Handler:new { result = m.config.result, loader = loader_config } - m.editor = Editor:new(m.handler, m.config.editor) - m.drawer = Drawer:new(m.handler, m.editor, m.config.drawer) + m.handler = Handler:new(result_ui, mem_loader, loaders) + m.result = Result:new(result_ui, m.config.result) + m.editor = Editor:new(editor_ui, m.handler, m.config.editor) + m.drawer = Drawer:new(drawer_ui, m.handler, m.editor, m.config.drawer) helpers.add(m.config.extra_helpers) end @@ -66,20 +100,21 @@ function M.setup(o) -- validate config vim.validate { connections = { opts.connections, "table" }, - loader_load = { opts.loader.load, "function" }, - loader_save = { opts.loader.add, "function" }, - loader_remove = { opts.loader.remove, "function" }, + connection_sources = { opts.connection_sources, "table" }, + connection_sources_files = { opts.connection_sources.files, "table" }, + connection_sources_env_vars = { opts.connection_sources.env_vars, "table" }, lazy = { opts.lazy, "boolean" }, extra_helpers = { opts.extra_helpers, "table" }, -- submodules - result_window_command = { opts.result.window_command, { "string", "function" } }, - editor_window_command = { opts.editor.window_command, { "string", "function" } }, editor_mappings = { opts.editor.mappings, "table" }, - drawer_window_command = { opts.drawer.window_command, { "string", "function" } }, drawer_disable_icons = { opts.drawer.disable_icons, "boolean" }, drawer_icons = { opts.drawer.icons, "table" }, drawer_mappings = { opts.drawer.mappings, "table" }, -- ui + ui_window_commands = { opts.ui.window_commands, "table" }, + ui_window_commands_drawer = { opts.ui.window_commands.drawer, { "string", "function" } }, + ui_window_commands_result = { opts.ui.window_commands.result, { "string", "function" } }, + ui_window_commands_editor = { opts.ui.window_commands.editor, { "string", "function" } }, ui_window_open_order = { opts.ui.window_open_order, "table" }, ui_pre_open_hook = { opts.ui.pre_open_hook, "function" }, ui_post_open_hook = { opts.ui.post_open_hook, "function" }, @@ -100,7 +135,7 @@ function M.add_connection(connection) if not pcall_lazy_setup() then return end - m.handler:add_connection(utils.expand_environment(connection) --[[@as connection_details]]) + m.handler:add_connection(connection) end function M.open() @@ -116,7 +151,7 @@ function M.open() local order_map = { drawer = m.drawer, - result = m.handler, + result = m.result, editor = m.editor, } @@ -138,7 +173,7 @@ function M.close() m.config.ui.pre_close_hook() - m.handler:close() + m.result:close() m.drawer:close() m.editor:close() @@ -150,14 +185,14 @@ function M.next() if not pcall_lazy_setup() then return end - m.handler:page_next() + m.handler:current_connection():page_next() end function M.prev() if not pcall_lazy_setup() then return end - m.handler:page_prev() + m.handler:current_connection():page_prev() end ---@param query string query to execute on currently selected connection @@ -165,7 +200,7 @@ function M.execute(query) if not pcall_lazy_setup() then return end - m.handler:execute(query) + m.handler:current_connection():execute(query) end ---@param format "csv"|"json" format of the output @@ -174,7 +209,7 @@ function M.save(format, file) if not pcall_lazy_setup() then return end - m.handler:save(format, file) + m.handler:current_connection():save(format, file) end ---@param command? "wget"|"curl"|"bitsadmin"|"go" preffered command diff --git a/lua/dbee/config.lua b/lua/dbee/config.lua index 67529f39..c5109b9a 100644 --- a/lua/dbee/config.lua +++ b/lua/dbee/config.lua @@ -4,9 +4,11 @@ local M = {} local m = {} ---@alias mapping {key: string, mode: string} +---@alias wincmd string|fun():integer ---@class UiConfig ----@field window_open_order table example: { "result", "editor", "drawer" } - in which order are the windows open +---@field window_commands { editor: wincmd, drawer: wincmd, result: wincmd } +---@field window_open_order string[] example: { "result", "editor", "drawer" } - in which order are the windows open ---@field pre_open_hook fun() execute this before opening ui ---@field post_open_hook fun() execute this after opening ui ---@field pre_close_hook fun() execute this before closing ui @@ -15,12 +17,12 @@ local m = {} -- configuration object ---@class Config ---@field connections connection_details[] list of configured database connections +---@field connection_sources { files: string[], env_vars: string[] } ---@field extra_helpers table extra table helpers to provide besides built-ins. example: { postgres = { List = "select..." } ---@field lazy boolean lazy load the plugin or not? ---@field drawer drawer_config ---@field editor editor_config ---@field result result_config ----@field loader loader_config ---@field ui UiConfig -- default configuration @@ -39,32 +41,12 @@ M.default = { -- url = "postgres://user:password@localhost:5432/db?sslmode=disable", -- }, }, - -- load/save functionality for connectoins - -- you can use helper function from require("dbee.loader") - -- or come up with something completly different - loader = { - -- you can control what happens with this function, - -- when the application wants to save connections - for example from "Add Connection" prompt - -- recieves a list of connections - add = function(connections) - -- append to default file - require("dbee.loader").add_to_file(connections) - end, - -- you can control what happens with this function, - -- when the application wants to remove connections - for example from drawer action - -- recieves a list of connections - remove = function(connections) - -- remove from default file - require("dbee.loader").remove_from_file(connections) - end, - -- use this function to provide different connections from files, env... - -- must return a list of connections - load = function() - -- load from default env var and file - local file_conns = require("dbee.loader").load_from_file() - local env_conns = require("dbee.loader").load_from_env() - return vim.list_extend(file_conns, env_conns) - end, + -- loads connections from files and environment variables + connection_sources = { + -- list of files to load connections from + files = {}, + -- list of env vars to load connections from + env_vars = {}, }, -- extra table helpers per connection type extra_helpers = { @@ -76,9 +58,6 @@ M.default = { -- drawer window config drawer = { - -- command that opens the window if the window is closed - -- string or function - window_command = "to 40vsplit", -- mappings for the buffer mappings = { -- manually refresh drawer @@ -153,9 +132,6 @@ M.default = { -- results window config result = { - -- command that opens the window if the window is closed - -- string or function - window_command = "bo 15split", -- number of rows per page page_size = 100, -- mappings for the buffer @@ -168,14 +144,6 @@ M.default = { -- editor window config editor = { - -- command that opens the window if the window is closed - -- string or function - window_command = function() - vim.cmd("new") - vim.cmd("only") - m.tmp_buf = vim.api.nvim_get_current_buf() - return vim.api.nvim_get_current_win() - end, -- mappings for the buffer mappings = { -- run what's currently selected on the active connection @@ -193,6 +161,18 @@ M.default = { -- -- You can probably do anything you imagine with this - for example all floating windows, tiled/floating mix etc. ui = { + -- commands that opens the window if the window is closed - for drawer/editor/result + -- string or function + window_commands = { + drawer = "to 40vsplit", + result = "bo 15split", + editor = function() + vim.cmd("new") + vim.cmd("only") + m.tmp_buf = vim.api.nvim_get_current_buf() + return vim.api.nvim_get_current_win() + end, + }, -- how to open windows in order (with specified "window_command"s -- see above) window_open_order = { "editor", "result", "drawer" }, diff --git a/lua/dbee/conn.lua b/lua/dbee/conn.lua new file mode 100644 index 00000000..b3aad95e --- /dev/null +++ b/lua/dbee/conn.lua @@ -0,0 +1,257 @@ +local helpers = require("dbee.helpers") +local utils = require("dbee.utils") + +---@alias conn_id string +---@alias connection_details { name: string, type: string, url: string, id: conn_id } +-- +---@class _LayoutGo +---@field name string display name +---@field type ""|"table"|"history" type of layout -> this infers action +---@field schema? string parent schema +---@field database? string parent database +---@field children? Layout[] child layout nodes + +-- Conn is a 1:1 mapping to go's connections +---@class Conn +---@field private ui Ui +---@field private __original connection_details original unmodified fields passed on initialization (params) +---@field private id conn_id +---@field private name string +---@field private type string --TODO enum? +---@field private page_index integer index of the current page +---@field private on_exec fun() callback which gets triggered on any action +local Conn = {} + +---@param ui Ui +---@param params connection_details +---@param opts? { page_size: integer, on_exec: fun() } +---@return Conn +function Conn:new(ui, params, opts) + params = params or {} + opts = opts or {} + + local expanded = utils.expand_environment(params) + + -- validation + if not ui then + error("no Ui provided to Conn!") + end + if not expanded.url then + error("url needs to be set!") + end + if not expanded.type or expanded.type == "" then + error("no type") + end + + -- get needed fields + local name = expanded.name + if not name or name == "" then + name = "[no name]" + end + local type = utils.type_alias(expanded.type) + local id = expanded.id or ("__master_connection_id_" .. expanded.name .. expanded.type .. "__") + params.id = id + + -- register in go + local ok = vim.fn.Dbee_register_connection(id, expanded.url, type, tostring(opts.page_size or 100)) + if not ok then + error("problem adding connection") + end + + -- class object + local o = { + ui = ui, + __original = params, + id = id, + name = name, + type = type, + page_index = 0, + on_exec = opts.on_exec or function() end, + } + setmetatable(o, self) + self.__index = self + return o +end + +function Conn:close() + -- TODO +end + +---@return connection_details +function Conn:details() + return { + id = self.id, + name = self.name, + -- url shouldn't be seen as expanded - it has secrets + url = self.__original.url, + type = self.type, + } +end + +---@return connection_details +function Conn:original_details() + return self.__original +end + +---@param query string query to execute +function Conn:execute(query) + self.on_exec() + + self:__wrap_open(function(_) + self.page_index = 0 + vim.fn.Dbee_execute(self.id, query) + return self.page_index, nil + end) +end + +---@param history_id string history id +function Conn:history(history_id) + self.on_exec() + + self:__wrap_open(function(_) + self.page_index = 0 + vim.fn.Dbee_history(self.id, history_id) + return self.page_index, nil + end) +end + +---@return integer # total number of pages +function Conn:page_next() + self.on_exec() + + local count + self:__wrap_open(function(_) + self.page_index, count = unpack(vim.fn.Dbee_page(self.id, tostring(self.page_index + 1))) + return self.page_index, count + end) + return count +end + +---@return integer # total number of pages +function Conn:page_prev() + self.on_exec() + + local count + self:__wrap_open(function(_) + self.page_index, count = unpack(vim.fn.Dbee_page(self.id, tostring(self.page_index - 1))) + return self.page_index, count + end) + return count +end + +---@param format "csv"|"json" how to format the result +---@param file string file to write to +function Conn:save(format, file) + if not format or not file then + error("save method requires format and file to be set") + end + vim.fn.Dbee_save(self.id, format, file) +end + +-- get layout for the connection +---@return Layout[] +function Conn:layout() + ---@param layout_go _LayoutGo[] layout from go + ---@return Layout[] layout with actions + local function to_layout(layout_go, parent_id) + if not layout_go or layout_go == vim.NIL then + return {} + end + + -- sort keys + table.sort(layout_go, function(k1, k2) + return k1.name < k2.name + end) + + local new_layouts = {} + for _, lgo in ipairs(layout_go) do + -- action 1 executes query or history + local action_1 + if lgo.type == "table" then + action_1 = function(cb) + local table_helpers = helpers.get(self.type) + local helper_keys = {} + for k, _ in pairs(table_helpers) do + table.insert(helper_keys, k) + end + table.sort(helper_keys) + -- select a helper to execute + vim.ui.select(helper_keys, { + prompt = "select a helper to execute:", + }, function(selection) + if selection then + self:execute( + helpers.expand_query( + table_helpers[selection], + { table = lgo.name, schema = lgo.schema, dbname = lgo.database } + ) + ) + end + cb() + end) + end + elseif lgo.type == "history" then + action_1 = function(cb) + self:history(lgo.name) + cb() + end + end + -- action 2 activates the connection manually TODO + local action_2 = function(cb) + cb() + end + -- action_3 is empty + + local l_id = (parent_id or "") .. "__connection_" .. lgo.name .. lgo.schema .. lgo.type .. "__" + local ly = { + id = l_id, + name = lgo.name, + schema = lgo.schema, + database = lgo.database, + type = lgo.type, + action_1 = action_1, + action_2 = action_2, + action_3 = nil, + children = to_layout(lgo.children, l_id), + } + + table.insert(new_layouts, ly) + end + + return new_layouts + end + + return to_layout(vim.fn.json_decode(vim.fn.Dbee_layout(self.id)), self.id) +end + +---@private +-- wraps a function to add buffer decorations +-- fn can optionally return page/total +---@param fn fun(bufnr: integer):integer?,integer? +function Conn:__wrap_open(fn) + -- open ui window + local winid, bufnr = self.ui:open() + + -- register buffer in go + vim.fn.Dbee_set_results_buf(bufnr) + + vim.api.nvim_buf_set_option(bufnr, "modifiable", true) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { "Loading..." }) + vim.api.nvim_buf_set_option(bufnr, "modifiable", false) + + local page, total = fn(bufnr) + + local tot = "?" + if total then + tot = tostring(total + 1) + end + local pg = "0" + if page then + pg = tostring(page + 1) + end + + -- set winbar + vim.api.nvim_win_set_option(winid, "winbar", "%=" .. pg .. "/" .. tot) +end + +return Conn diff --git a/lua/dbee/drawer.lua b/lua/dbee/drawer.lua index 5fc77ba3..b52c5656 100644 --- a/lua/dbee/drawer.lua +++ b/lua/dbee/drawer.lua @@ -20,46 +20,33 @@ local NuiLine = require("nui.line") ---@class Node: Layout ---@field getter fun():Layout ----@alias drawer_config { disable_icons: boolean, icons: table, mappings: table, window_command: string|fun():integer } +---@alias drawer_config { disable_icons: boolean, icons: table, mappings: table } ---@class Drawer +---@field private ui Ui ---@field private tree table NuiTree ---@field private handler Handler ---@field private editor Editor ---@field private mappings table ----@field private bufnr integer ----@field private winid integer ---@field private icons table ----@field private win_cmd fun():integer function which opens a new window and returns a window id local Drawer = {} +---@param ui Ui ---@param handler Handler ---@param editor Editor ---@param opts? drawer_config ---@return Drawer -function Drawer:new(handler, editor, opts) +function Drawer:new(ui, handler, editor, opts) opts = opts or {} + if not ui then + error("no Ui provided to Drawer") + end if not handler then - error("no Handler provided to drawer") + error("no Handler provided to Drawer") end if not editor then - error("no Editor provided to drawer") - end - - local win_cmd - if type(opts.window_command) == "string" then - win_cmd = function() - vim.cmd(opts.window_command) - return vim.api.nvim_get_current_win() - end - elseif type(opts.window_command) == "function" then - win_cmd = opts.window_command - else - win_cmd = function() - vim.cmd("to 40vsplit") - return vim.api.nvim_get_current_win() - end + error("no Editor provided to Drawer") end local icons = {} @@ -69,12 +56,12 @@ function Drawer:new(handler, editor, opts) -- class object local o = { + ui = ui, tree = nil, handler = handler, editor = editor, mappings = opts.mappings or {}, icons = icons, - win_cmd = win_cmd, } setmetatable(o, self) self.__index = self @@ -120,8 +107,7 @@ function Drawer:create_tree(bufnr) end -- if connection is the active one, apply a special highlight on the master - local active = self.handler:connection_details() - if active and active.id == node.id then + if self.handler:current_connection():details().id == node.id then line:append(node.name, icon.highlight) else line:append(node.name) @@ -227,7 +213,6 @@ function Drawer:actions() } end --- Map keybindings to split window ---@private ---@param bufnr integer which buffer to map the keys in function Drawer:map_keys(bufnr) @@ -356,39 +341,7 @@ end -- Show drawer on screen function Drawer:open() - if not self.winid or not vim.api.nvim_win_is_valid(self.winid) then - self.winid = self.win_cmd() - end - - -- if buffer doesn't exist, create it - local bufnr = self.bufnr - if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then - bufnr = vim.api.nvim_create_buf(false, true) - end - - vim.api.nvim_win_set_buf(self.winid, bufnr) - vim.api.nvim_set_current_win(self.winid) - vim.api.nvim_buf_set_name(bufnr, "dbee-drawer") - - -- set options - local buf_opts = { - buflisted = false, - bufhidden = "delete", - buftype = "nofile", - swapfile = false, - } - local win_opts = { - wrap = false, - winfixheight = true, - winfixwidth = true, - number = false, - } - for opt, val in pairs(buf_opts) do - vim.api.nvim_buf_set_option(bufnr, opt, val) - end - for opt, val in pairs(win_opts) do - vim.api.nvim_win_set_option(self.winid, opt, val) - end + local _, bufnr = self.ui:open() -- tree if not self.tree then @@ -399,13 +352,11 @@ function Drawer:open() self:map_keys(bufnr) self.tree.bufnr = bufnr - self.bufnr = bufnr - self.tree:render() end function Drawer:close() - pcall(vim.api.nvim_win_close, self.winid, false) + self.ui:close() end return Drawer diff --git a/lua/dbee/editor.lua b/lua/dbee/editor.lua index 882411d0..6facb4f8 100644 --- a/lua/dbee/editor.lua +++ b/lua/dbee/editor.lua @@ -4,40 +4,28 @@ local SCRATCHES_DIR = vim.fn.stdpath("cache") .. "/dbee/scratches" ---@alias scratch_id string ---@alias scratch_details { file: string, bufnr: integer, type: "file"|"buffer", id: scratch_id } ----@alias editor_config { mappings: table, window_command: string|fun():integer } +---@alias editor_config { mappings: table } ---@class Editor +---@field private ui Ui ---@field private handler Handler ---@field private mappings table ---@field private scratches table id - scratch mapping ---@field private active_scratch scratch_id id of the current scratch ----@field private winid integer ----@field private win_cmd fun():integer function which opens a new window and returns a window id local Editor = {} +---@param ui Ui ---@param handler Handler ---@param opts? editor_config ---@return Editor -function Editor:new(handler, opts) +function Editor:new(ui, handler, opts) opts = opts or {} - if not handler then - error("no Handler provided to editor") + if not ui then + error("no Ui provided to Editor") end - - local win_cmd - if type(opts.window_command) == "string" then - win_cmd = function() - vim.cmd(opts.window_command) - return vim.api.nvim_get_current_win() - end - elseif type(opts.window_command) == "function" then - win_cmd = opts.window_command - else - win_cmd = function() - vim.cmd("split") - return vim.api.nvim_get_current_win() - end + if not handler then + error("no Handler provided to Editor") end -- check for any existing scratches @@ -60,26 +48,17 @@ function Editor:new(handler, opts) -- class object local o = { + ui = ui, handler = handler, - winid = nil, scratches = scratches, active_scratch = active, mappings = opts.mappings or {}, - win_cmd = win_cmd, } setmetatable(o, self) self.__index = self return o end ----@return boolean -function Editor:is_current_window() - if self.winid == vim.api.nvim_get_current_win() then - return true - end - return false -end - function Editor:new_scratch() local file = SCRATCHES_DIR .. "/scratch." .. tostring(os.clock()) .. ".sql" local id = file .. tostring(os.clock()) @@ -227,7 +206,7 @@ function Editor:actions() local lines = vim.api.nvim_buf_get_lines(bnr, 0, -1, false) local query = table.concat(lines, "\n") - self.handler:execute(query) + self.handler:current_connection():execute(query) end, run_selection = function() local srow, scol, erow, ecol = utils.visual_selection() @@ -235,7 +214,7 @@ function Editor:actions() local selection = vim.api.nvim_buf_get_text(0, srow, scol, erow, ecol, {}) local query = table.concat(selection, "\n") - self.handler:execute(query) + self.handler:current_connection():execute(query) end, } end @@ -255,11 +234,10 @@ function Editor:map_keys(bufnr) end function Editor:open() - if not self.winid or not vim.api.nvim_win_is_valid(self.winid) then - self.winid = self.win_cmd() - end + -- each scratchpad is it's own buffer, so we can ignore ui's bufnr + local winid, _ = self.ui:open() - vim.api.nvim_set_current_win(self.winid) + vim.api.nvim_set_current_win(winid) -- get current scratch details local id = self.active_scratch @@ -272,7 +250,7 @@ function Editor:open() -- if file doesn't exist, open new buffer and update list on save if vim.fn.filereadable(s.file) ~= 1 then bufnr = s.bufnr or vim.api.nvim_create_buf(true, false) - vim.api.nvim_win_set_buf(self.winid, bufnr) + vim.api.nvim_win_set_buf(winid, bufnr) -- automatically fill the name of the file when saving for the first time vim.keymap.set("c", "w", function() @@ -298,7 +276,7 @@ function Editor:open() else -- just open the file bufnr = s.bufnr or vim.api.nvim_create_buf(true, false) - vim.api.nvim_win_set_buf(self.winid, bufnr) + vim.api.nvim_win_set_buf(winid, bufnr) vim.cmd("e " .. s.file) end @@ -319,7 +297,7 @@ function Editor:open() end function Editor:close() - pcall(vim.api.nvim_win_close, self.winid, false) + self.ui:close() end return Editor diff --git a/lua/dbee/handler.lua b/lua/dbee/handler.lua index 51700c86..b04f7a24 100644 --- a/lua/dbee/handler.lua +++ b/lua/dbee/handler.lua @@ -1,307 +1,137 @@ -local helpers = require("dbee.helpers") local utils = require("dbee.utils") +local Conn = require("dbee.conn") +local Lookup = require("dbee.lookup") ----@alias conn_id string ----@alias connection_details { name: string, type: string, url: string, id: conn_id } --- ----@class _LayoutGo ----@field name string display name ----@field type ""|"table"|"history" type of layout -> this infers action ----@field schema? string parent schema ----@field database? string parent database ----@field children? Layout[] child layout nodes --- ----@alias result_config { mappings: table, page_size: integer, window_command: string|fun():integer } ----@alias loader_config { add: fun(conns: connection_details[]), remove: fun(conns: connection_details[]), load: fun():connection_details[] } +---@alias loader_id string --- Handler is a wrapper around the go code --- it is the central part of the plugin and manages connections. --- almost all functions take the connection id as their argument. +-- Handler is an aggregator of connections ---@class Handler ----@field private connections table id - connection mapping ----@field private active_connection conn_id last called connection ----@field private page_index integer current page ----@field private winid integer ----@field private bufnr integer ----@field private win_cmd fun():integer function which opens a new window and returns a window id ----@field private page_size integer number of rows per page ----@field private mappings table ----@field private loader_add fun(conns: connection_details[]) function to add connections to external source ----@field private loader_remove fun(conns: connection_details[]) function to remove connections from external source ----@field private loader_load fun():connection_details[] function to load connections from external source +---@field private ui Ui ui for results +---@field private lookup Lookup lookup for loaders and connections +---@field private default_loader_id string local Handler = {} ----@param opts? { result: result_config, loader: loader_config } +---@param ui Ui ui for displaying results +---@param default_loader Loader +---@param other_loaders? Loader[] ---@return Handler -function Handler:new(opts) - opts = opts or {} - opts.result = opts.result or {} - opts.loader = opts.loader or {} - - local page_size = opts.result.page_size or 100 - - local win_cmd - if type(opts.result.window_command) == "string" then - win_cmd = function() - vim.cmd(opts.result.window_command) - return vim.api.nvim_get_current_win() - end - elseif type(opts.result.window_command) == "function" then - win_cmd = opts.result.window_command - else - win_cmd = function() - vim.cmd("bo 15split") - return vim.api.nvim_get_current_win() - end +function Handler:new(ui, default_loader, other_loaders) + if not ui then + error("no results Ui passed to Handler") + end + if not default_loader then + error("no default Loader passed to Handler") end -- class object local o = { - connections = {}, - active_connection = "", - page_index = 0, - win_cmd = win_cmd, - page_size = page_size, - mappings = opts.result.mappings or {}, - loader_add = opts.loader.add or function() end, - loader_remove = opts.loader.remove or function() end, - loader_load = opts.loader.load or function() end, + ui = ui, + lookup = Lookup:new(), + default_loader_id = default_loader:name(), } setmetatable(o, self) self.__index = self - -- initialize connections from loader - local conns = o.loader_load() - for _, conn in ipairs(conns) do - pcall(o.add_connection, o, utils.expand_environment(conn)) + -- initialize the default loader and others + o:loader_add(default_loader) + + if other_loaders then + for _, loader in ipairs(other_loaders) do + pcall(o.loader_add, o, loader) + end end return o end ----@param connection connection_details ----@return conn_id # id of the added connection -function Handler:add_connection(connection) - if not connection.url then - error("url needs to be set!") - end - if not connection.type or connection.type == "" then - error("no type") - end - - connection.name = connection.name or "[empty name]" - connection.type = utils.type_alias(connection.type) - connection.id = connection.id or ("__master_connection_id_" .. connection.name .. connection.type .. "__") - - -- register in go - local ok = vim.fn.Dbee_register_connection(connection.id, connection.url, connection.type, tostring(self.page_size)) - if not ok then - error("problem adding connection") - end - - self.connections[connection.id] = connection - self.active_connection = connection.id - - return connection.id +-- add new source and load connections from it +---@param loader Loader +function Handler:loader_add(loader) + local id = loader:name() + -- add it + self.lookup:add_loader(loader) + -- and load it's connections + self:loader_reload(id) end --- removes/unregisters connection ----@param id conn_id connection id -function Handler:remove_connection(id) - if not id then +---@param id loader_id +function Handler:loader_reload(id) + local loader = self.lookup:get_loader(id) + if not loader then return end - self.connections[id] = nil - if self.active_connection == id then - self.active_connection = utils.random_key(self.connections) - end -end + local specs = loader:load() ----@param id conn_id connection id -function Handler:set_active(id) - if not id or self.connections[id] == nil then - error("no id specified!") + for _, spec in ipairs(specs) do + self:add_connection(spec, id) end - self.active_connection = id end ----@return connection_details[] list of connections -function Handler:list_connections() - local cons = {} - for _, con in pairs(self.connections) do - table.insert(cons, con) +--- adds connection +---@param params connection_details +---@param loader_id? loader_id id of the loader to save connection to +---@return conn_id # id of the added connection +function Handler:add_connection(params, loader_id) + loader_id = loader_id or self.default_loader_id + -- create a new connection + ---@type Conn + local conn, ok + ok, conn = pcall(Conn.new, Conn, self.ui, params, { + page_size = 100, --[[TODO]] + on_exec = function() + self:set_active(conn:details().id) + end, + }) + if not ok then + utils.log("error", tostring(conn), "handler") + return "" end - -- sort keys - table.sort(cons, function(k1, k2) - return k1.name < k2.name - end) - return cons -end + -- remove it if the same one exists + self:remove_connection(conn:details().id) ----@return connection_details ----@param id? conn_id connection id -function Handler:connection_details(id) - id = id or self.active_connection - return self.connections[id] -end + -- add it to lookup + self.lookup:add_connection(conn, loader_id) ----@param query string query to execute ----@param id? conn_id connection id -function Handler:execute(query, id) - id = id or self.active_connection - - self:open_pre_hook() - self.page_index = 0 - vim.fn.Dbee_execute(id, query) - self:open_post_hook() -end + -- save it to loader if it exists + local loader = self.lookup:get_loader(loader_id) + if loader and type(loader.save) == "function" then + loader:save({ conn:original_details() }, "add") + end ----@private --- called before anything needs to be displayed on screen -function Handler:open_pre_hook() - self:open() - vim.api.nvim_buf_set_option(self.bufnr, "modifiable", true) - vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, true, { "Loading..." }) - vim.api.nvim_buf_set_option(self.bufnr, "modifiable", false) + return conn:details().id end ----@private --- called after anything needs to be displayed on screen ----@param count? integer total number of pages -function Handler:open_post_hook(count) - if not self.winid or not vim.api.nvim_win_is_valid(self.winid) then +-- removes/unregisters connection +-- also deletes it from the loader if it exists +---@param id conn_id connection id +function Handler:remove_connection(id) + local conn = self.lookup:get_connection(id) + if not conn then return end - local total = "?" - if count then - total = tostring(count + 1) - end - local index = "0" - if self.page_index then - index = tostring(self.page_index + 1) - end - - -- set winbar - vim.api.nvim_win_set_option(self.winid, "winbar", "%=" .. index .. "/" .. total) -end + local original_details = conn:original_details() ----@param id? conn_id connection id -function Handler:page_next(id) - id = id or self.active_connection + -- delete it + self.lookup:remove_connection(id) - self:open_pre_hook() - local count - self.page_index, count = unpack(vim.fn.Dbee_page(id, tostring(self.page_index + 1))) - self:open_post_hook(count) -end - ----@param id? conn_id connection id -function Handler:page_prev(id) - id = id or self.active_connection - - self:open_pre_hook() - local count - self.page_index, count = unpack(vim.fn.Dbee_page(id, tostring(self.page_index - 1))) - self:open_post_hook(count) + -- delete it from the loader + local loader = self.lookup:get_loaders(id)[1] + if loader and type(loader.save) == "function" then + loader:save({ original_details }, "delete") + end end ----@param history_id string history id ----@param id? conn_id connection id -function Handler:history(history_id, id) - id = id or self.active_connection - - self:open_pre_hook() - self.page_index = 0 - vim.fn.Dbee_history(id, history_id) - self:open_post_hook() +---@param id conn_id connection id +function Handler:set_active(id) + self.lookup:set_active_connection(id) end --- get layout for the connection ----@private ----@param id? conn_id connection id ----@return Layout[] -function Handler:get_connection_layout(id) - id = id or self.active_connection - - ---@param layout_go _LayoutGo[] layout from go - ---@return Layout[] layout with actions - local function to_layout(layout_go, parent_id) - if not layout_go or layout_go == vim.NIL then - return {} - end - - -- sort keys - table.sort(layout_go, function(k1, k2) - return k1.name < k2.name - end) - - local new_layouts = {} - for _, lgo in ipairs(layout_go) do - -- action 1 executes query or history - local action_1 - if lgo.type == "table" then - action_1 = function(cb) - local details = self:connection_details(id) - local table_helpers = helpers.get(details.type) - local helper_keys = {} - for k, _ in pairs(table_helpers) do - table.insert(helper_keys, k) - end - table.sort(helper_keys) - -- select a helper to execute - vim.ui.select(helper_keys, { - prompt = "select a helper to execute:", - }, function(selection) - if selection then - self:execute( - helpers.expand_query( - table_helpers[selection], - { table = lgo.name, schema = lgo.schema, dbname = lgo.database } - ), - id - ) - end - cb() - end) - self:set_active(id) - end - elseif lgo.type == "history" then - action_1 = function(cb) - self:history(lgo.name, id) - self:set_active(id) - cb() - end - end - -- action 2 activates the connection manually - local action_2 = function(cb) - self:set_active(id) - cb() - end - -- action_3 is empty - - local l_id = (parent_id or "") .. "__connection_" .. lgo.name .. lgo.schema .. lgo.type .. "__" - local ly = { - id = l_id, - name = lgo.name, - schema = lgo.schema, - database = lgo.database, - type = lgo.type, - action_1 = action_1, - action_2 = action_2, - action_3 = nil, - children = to_layout(lgo.children, l_id), - } - - table.insert(new_layouts, ly) - end - - return new_layouts - end - - return to_layout(vim.fn.json_decode(vim.fn.Dbee_layout(id)), id) +---@return Conn # currently active connection +function Handler:current_connection() + return self.lookup:get_active_connection() end ---@return Layout[] @@ -309,29 +139,29 @@ function Handler:layout() ---@type Layout[] local layout = {} - for _, conn in ipairs(self:list_connections()) do + for _, conn in ipairs(self.lookup:get_connections()) do + local details = conn:details() table.insert(layout, { - id = conn.id, - name = conn.name, + id = details.id, + name = details.name, type = "database", -- set connection as active manually action_2 = function(cb) - self:set_active(conn.id) + self:set_active(details.id) cb() end, -- remove connection (also trigger the loader function) action_3 = function(cb) - vim.ui.input({ prompt = 'confirm deletion of "' .. conn.name .. '"', default = "Y" }, function(input) + vim.ui.input({ prompt = 'confirm deletion of "' .. details.name .. '"', default = "Y" }, function(input) if not input or string.lower(input) ~= "y" then return end - self:remove_connection(conn.id) - self.loader_remove { conn } + self:remove_connection(details.id) cb() end) end, children = function() - return self:get_connection_layout(conn.id) + return conn:layout() end, }) end @@ -350,12 +180,7 @@ function Handler:layout() utils.prompt.open(prompt, { title = "Add Connection", callback = function(result) - local ok, added_id = - pcall(self.add_connection, self, utils.expand_environment(result) --[[@as connection_details]]) - if ok then - result.id = added_id - self.loader_add { result } - end + pcall(self.add_connection, self, result --[[@as connection_details]]) cb() end, }) @@ -365,79 +190,4 @@ function Handler:layout() return layout end ----@param format "csv"|"json" how to format the result ----@param file string file to write to ----@param id? conn_id connection id -function Handler:save(format, file, id) - id = id or self.active_connection - if not format or not file then - error("save method requires format and file to be set") - end - vim.fn.Dbee_save(id, format, file) -end - ----@return table -function Handler:actions() - return { - page_next = function() - self:page_next() - end, - page_prev = function() - self:page_prev() - end, - } -end - ----@private -function Handler:map_keys(bufnr) - local map_options = { noremap = true, nowait = true, buffer = bufnr } - - local actions = self:actions() - - for act, map in pairs(self.mappings) do - local action = actions[act] - if action and type(action) == "function" then - vim.keymap.set(map.mode, map.key, action, map_options) - end - end -end - --- fill the Ui interface - open results -function Handler:open() - if not self.winid or not vim.api.nvim_win_is_valid(self.winid) then - self.winid = self.win_cmd() - end - - -- if buffer doesn't exist, create it - local bufnr = self.bufnr - if not bufnr or not vim.api.nvim_buf_is_valid(bufnr) then - bufnr = vim.api.nvim_create_buf(false, true) - end - vim.api.nvim_win_set_buf(self.winid, bufnr) - vim.api.nvim_set_current_win(self.winid) - vim.api.nvim_buf_set_name(bufnr, "dbee-results-" .. tostring(os.clock())) - - -- set keymaps - self:map_keys(bufnr) - - local win_opts = { - wrap = false, - winfixheight = true, - winfixwidth = true, - number = false, - } - for opt, val in pairs(win_opts) do - vim.api.nvim_win_set_option(self.winid, opt, val) - end - - self.bufnr = bufnr - - -- register in go - vim.fn.Dbee_set_results_buf(bufnr) -end - -function Handler:close() - pcall(vim.api.nvim_win_close, self.winid, false) -end - return Handler diff --git a/lua/dbee/loader.lua b/lua/dbee/loader.lua index b23de632..983ca27c 100644 --- a/lua/dbee/loader.lua +++ b/lua/dbee/loader.lua @@ -4,11 +4,36 @@ local DEFAULT_PERSISTENCE_FILE = vim.fn.stdpath("cache") .. "/dbee/persistence.j local M = {} --- Parses json file with connections +---@class Loader +---@field name fun(self: Loader):string function to return the name of the loader +---@field load fun(self: Loader):connection_details[] function to load connections from external source +---@field save? fun(self: Loader, conns: connection_details[], action: "add"|"delete") function to save connections to external source (optional) + +--- File loader +---@class FileLoader: Loader +---@field private path string path to file +M.FileLoader = {} + +--- Loads connections from json file ---@param path? string path to file +---@return Loader +function M.FileLoader:new(path) + local o = { + path = path or DEFAULT_PERSISTENCE_FILE, + } + setmetatable(o, self) + self.__index = self + return o +end + +---@return string +function M.FileLoader:name() + return vim.fs.basename(self.path) +end + ---@return connection_details[] -function M.load_from_file(path) - path = path or DEFAULT_PERSISTENCE_FILE +function M.FileLoader:load() + local path = self.path ---@type connection_details[] local conns = {} @@ -40,50 +65,50 @@ function M.load_from_file(path) return conns end --- Parses env variable if it exists ----@param var? string env var to check - default: DBEE_CONNECTIONS ----@return connection_details[] -function M.load_from_env(var) - var = var or "DBEE_CONNECTIONS" - - ---@type connection_details[] - local conns = {} - - local raw = os.getenv(var) - if not raw then - return {} - end - - local ok, data = pcall(vim.fn.json_decode, raw) - if not ok then - utils.log("warn", 'Could not parse connections from env: "' .. var .. '".', "loader") - return {} - end +-- saves connection to file +---@param conns connection_details[] +---@param action "add"|"delete" +function M.FileLoader:save(conns, action) + local path = self.path - for _, conn in pairs(data) do - if type(conn) == "table" and conn.url and conn.type then - table.insert(conns, conn) - end + if not conns or vim.tbl_isempty(conns) then + return end - return conns -end + -- read from file + local existing = self:load() --- appends connection_details to a json ----@param connections connection_details[] ----@param path? string path to save file -function M.add_to_file(connections, path) - path = path or DEFAULT_PERSISTENCE_FILE + ---@type connection_details[] + local new = {} + + if action == "add" then + for _, to_add in ipairs(conns) do + local edited = false + for i, ex_conn in ipairs(existing) do + if to_add.id == ex_conn.id then + existing[i] = to_add + edited = true + end + end - if not connections or vim.tbl_isempty(connections) then - return + if not edited then + table.insert(existing, to_add) + end + end + new = existing + elseif action == "delete" then + for _, to_remove in ipairs(conns) do + for i, ex_conn in ipairs(existing) do + if to_remove.id == ex_conn.id then + table.remove(existing, i) + end + end + end + new = existing end - local existing = M.load_from_file(path) - - existing = vim.list_extend(existing, connections) - - local ok, json = pcall(vim.fn.json_encode, existing) + -- write back to file + local ok, json = pcall(vim.fn.json_encode, new) if not ok then utils.log("error", "Could not convert connection list to json", "loader") return @@ -95,36 +120,78 @@ function M.add_to_file(connections, path) file:close() end --- removes connection_details from a json file ----@param connections connection_details[] ----@param path? string path to save file -function M.remove_from_file(connections, path) - path = path or DEFAULT_PERSISTENCE_FILE +--- Environment loader +---@class EnvLoader: Loader +---@field private var string path to file +M.EnvLoader = {} + +--- Loads connections from json file +---@param var? string env var to load from +---@return Loader +function M.EnvLoader:new(var) + local o = { + var = var or "DBEE_CONNECTIONS", + } + setmetatable(o, self) + self.__index = self + return o +end - if not connections or vim.tbl_isempty(connections) then - return - end +---@return string +function M.EnvLoader:name() + return self.var +end - local existing = M.load_from_file(path) +---@return connection_details[] +function M.EnvLoader:load() + ---@type connection_details[] + local conns = {} - for _, to_remove in ipairs(connections) do - for i, ex_conn in ipairs(existing) do - if to_remove.id == ex_conn.id then - table.remove(existing, i) - end - end + local raw = os.getenv(self.var) + if not raw then + return {} end - local ok, json = pcall(vim.fn.json_encode, existing) + local ok, data = pcall(vim.fn.json_decode, raw) if not ok then - utils.log("error", "Could not convert connection list to json", "loader") - return + utils.log("warn", 'Could not parse connections from env: "' .. self.var .. '".', "loader") + return {} end - -- overwrite file - local file = assert(io.open(path, "w+"), "could not open file") - file:write(json) - file:close() + for _, conn in pairs(data) do + if type(conn) == "table" and conn.url and conn.type then + table.insert(conns, conn) + end + end + + return conns +end + +--- Environment loader +---@class MemoryLoader: Loader +---@field conns connection_details[] +M.MemoryLoader = {} + +--- Loads connections from json file +---@param conns connection_details[] +---@return Loader +function M.MemoryLoader:new(conns) + local o = { + conns = conns or {}, + } + setmetatable(o, self) + self.__index = self + return o +end + +---@return string +function M.MemoryLoader:name() + return "memory" +end + +---@return connection_details[] +function M.MemoryLoader:load() + return self.conns end return M diff --git a/lua/dbee/lookup.lua b/lua/dbee/lookup.lua new file mode 100644 index 00000000..bafe4dda --- /dev/null +++ b/lua/dbee/lookup.lua @@ -0,0 +1,185 @@ +local utils = require("dbee.utils") + +-- Lookup is a "dumb" storage for loaders and connections +-- and their relations +---@class Lookup +---@field private connections table +---@field private loaders table +---@field private conn_lookup table +---@field private active_connection conn_id +local Lookup = {} + +---@return Lookup +function Lookup:new() + local o = { + connections = {}, + loaders = {}, + conn_lookup = {}, + } + setmetatable(o, self) + self.__index = self + return o +end + +---@param loader Loader +function Lookup:add_loader(loader) + local id = loader:name() + + if self.loaders[id] then + error("loader already exists: " .. id) + end + + self.loaders[id] = { + loader = loader, + connections = {}, + active_connection = "", + } +end + +---@param id loader_id +function Lookup:remove_loader(id) + if not self.loaders[id] then + return + end + + for _, conn_id in ipairs(self.loaders[id].connections) do + local conn = self.connections[conn_id] + if conn then + pcall(conn.close, conn) + end + self.connections[conn_id] = nil + self.conn_lookup[conn_id] = nil + end + + self.loaders[id] = nil +end + +---@param connection Conn +---@param loader_id loader_id +function Lookup:add_connection(connection, loader_id) + if not loader_id then + error("loader_id not set") + end + + local id = connection:details().id + + self:remove_connection(id) + + self.connections[id] = connection + self.conn_lookup[loader_id] = id + table.insert(self.loaders[loader_id].connections, id) + + self.active_connection = id +end + +---@param id conn_id +function Lookup:remove_connection(id) + local conn = self.connections[id] + if not conn then + return + end + + -- close the connection + pcall(conn.close, conn) + + -- remove the connection from all lookups + local loader_id = self.conn_lookup[id] + if self.loaders[loader_id] and self.loaders[loader_id].connections then + for i, c_id in ipairs(self.loaders[loader_id].connections) do + if id == c_id then + table.remove(self.loaders[loader_id].connections, i) + end + end + end + self.conn_lookup[id] = nil + self.connections[id] = nil + + -- set random connection as active + if self.active_connection == id then + self.active_connection = utils.random_key(self.connections) + end +end + +---@param loader_id? loader_id # id of the loader or all +---@return Conn[] connections +function Lookup:get_connections(loader_id) + local conns = {} + -- get connections of a loader + -- or get all connections + if loader_id then + local l = self.loaders[loader_id] + if not l then + error("unknown loader: " .. loader_id) + end + for _, c_id in ipairs(l.connections) do + table.insert(conns, self.connections[c_id]) + end + else + for _, conn in pairs(self.connections) do + table.insert(conns, conn) + end + end + + -- sort keys + table.sort(conns, function(k1, k2) + return k1:details().name < k2:details().name + end) + + return conns +end + +---@param id conn_id +---@return Conn|nil connection +function Lookup:get_connection(id) + return self.connections[id] +end + +---@return Conn +function Lookup:get_active_connection() + return self.connections[self.active_connection] +end + +---@param id conn_id +function Lookup:set_active_connection(id) + if self.connections[id] then + self.active_connection = id + end +end + +---@param conn_id? conn_id id of the connection or all +---@return Loader[] loaders +function Lookup:get_loaders(conn_id) + local loaders = {} + -- get loader of a connection + -- or get all loaders + if conn_id then + local l_id = self.conn_lookup[conn_id] + if not l_id then + return {} + end + table.insert(loaders, self.loaders[l_id].loader) + else + for _, l in pairs(self.loaders) do + table.insert(loaders, l.loader) + end + end + + -- sort keys + table.sort(loaders, function(k1, k2) + return k1:name() < k2:name() + end) + + return loaders +end + +---@param id loader_id +---@return Loader|nil loader +function Lookup:get_loader(id) + local l = self.loaders[id] + if not l then + return + end + return l.loader +end + +return Lookup diff --git a/lua/dbee/result.lua b/lua/dbee/result.lua new file mode 100644 index 00000000..9843424f --- /dev/null +++ b/lua/dbee/result.lua @@ -0,0 +1,83 @@ +---@alias result_config { mappings: table, page_size: integer } + +-- Result is a wrapper around the go code +-- it is the central part of the plugin and manages connections. +-- almost all functions take the connection id as their argument. +---@class Result +---@field private ui Ui +---@field private handler Handler +---@field private mappings table +---@field private size integer number of rows per page +local Result = {} + +---@param ui Ui +---@param handler Handler +---@param opts? result_config +---@return Result +function Result:new(ui, handler, opts) + opts = opts or {} + + if not handler then + error("no Handler passed to Result") + end + if not ui then + error("no Ui passed to Result") + end + + local page_size = opts.page_size or 100 + + -- class object + local o = { + ui = ui, + handler = handler, + size = page_size, + mappings = opts.mappings or {}, + } + setmetatable(o, self) + self.__index = self + return o +end + +---@return integer # size of one page +function Result:page_size() + return self.size +end + +---@return table +function Result:actions() + return { + page_next = function() + self.handler:current_connection():page_next() + end, + page_prev = function() + self.handler:current_connection():page_prev() + end, + } +end + +---@private +function Result:map_keys(bufnr) + local map_options = { noremap = true, nowait = true, buffer = bufnr } + + local actions = self:actions() + + for act, map in pairs(self.mappings) do + local action = actions[act] + if action and type(action) == "function" then + vim.keymap.set(map.mode, map.key, action, map_options) + end + end +end + +function Result:open() + local _, bufnr = self.ui:open() + + -- set keymaps + self:map_keys(bufnr) +end + +function Result:close() + self.ui:close() +end + +return Result diff --git a/lua/dbee/ui.lua b/lua/dbee/ui.lua new file mode 100644 index 00000000..63ef8c53 --- /dev/null +++ b/lua/dbee/ui.lua @@ -0,0 +1,84 @@ +---@alias ui_config { buffer_options: table, window_options: table, window_command: string|fun():integer } + +---@class Ui +---@field private winid integer +---@field private bufnr integer +---@field private window_options table +---@field private buffer_options table +---@field private window_command fun():integer function which opens a new window and returns a window id +local Ui = {} + +---@param opts? ui_config +---@return Ui +function Ui:new(opts) + opts = opts or {} + + local win_cmd + if type(opts.window_command) == "string" then + win_cmd = function() + vim.cmd(opts.window_command) + return vim.api.nvim_get_current_win() + end + elseif type(opts.window_command) == "function" then + win_cmd = opts.window_command + else + win_cmd = function() + vim.cmd("vsplit") + return vim.api.nvim_get_current_win() + end + end + + -- class object + local o = { + winid = nil, + bufnr = nil, + window_command = win_cmd, + window_options = opts.window_options or {}, + buffer_options = opts.buffer_options or {}, + } + setmetatable(o, self) + self.__index = self + return o +end + +---@return integer winid +function Ui:window() + return self.winid +end + +---@return integer bufnr +function Ui:buffer() + return self.bufnr +end + +---@return integer winid +---@return integer bufnr +function Ui:open() + if not self.winid or not vim.api.nvim_win_is_valid(self.winid) then + self.winid = self.window_command() + end + + -- if buffer doesn't exist, create it + if not self.bufnr or not vim.api.nvim_buf_is_valid(self.bufnr) then + self.bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(self.bufnr, "dbee-" .. tostring(os.clock())) + end + vim.api.nvim_win_set_buf(self.winid, self.bufnr) + vim.api.nvim_set_current_win(self.winid) + + -- set options + for opt, val in pairs(self.buffer_options) do + vim.api.nvim_buf_set_option(self.bufnr, opt, val) + end + for opt, val in pairs(self.window_options) do + vim.api.nvim_win_set_option(self.winid, opt, val) + end + + return self.winid, self.bufnr +end + +function Ui:close() + pcall(vim.api.nvim_win_close, self.winid, false) +end + +return Ui From b2c3c10fff5228ab4e6b3c9ca6922543f62d7c6e Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Sat, 20 May 2023 18:52:24 +0200 Subject: [PATCH 16/25] - display connections per loader - generalized drawer icons to candy - handle keymaps in ui - other fixes --- lua/dbee.lua | 6 +- lua/dbee/config.lua | 36 ++++--- lua/dbee/conn.lua | 13 ++- lua/dbee/drawer.lua | 198 ++++++++++++++++++++------------------ lua/dbee/editor.lua | 62 ++++++------ lua/dbee/handler.lua | 158 +++++++++++++++++++----------- lua/dbee/loader.lua | 24 +++-- lua/dbee/lookup.lua | 7 +- lua/dbee/result.lua | 59 +++++------- lua/dbee/ui.lua | 36 +++++++ lua/dbee/utils/prompt.lua | 22 ++--- 11 files changed, 364 insertions(+), 257 deletions(-) diff --git a/lua/dbee.lua b/lua/dbee.lua index 85b88239..669c0567 100644 --- a/lua/dbee.lua +++ b/lua/dbee.lua @@ -69,7 +69,7 @@ local function lazy_setup() -- set up modules m.handler = Handler:new(result_ui, mem_loader, loaders) - m.result = Result:new(result_ui, m.config.result) + m.result = Result:new(result_ui, m.handler, m.config.result) m.editor = Editor:new(editor_ui, m.handler, m.config.editor) m.drawer = Drawer:new(drawer_ui, m.handler, m.editor, m.config.drawer) @@ -107,8 +107,8 @@ function M.setup(o) extra_helpers = { opts.extra_helpers, "table" }, -- submodules editor_mappings = { opts.editor.mappings, "table" }, - drawer_disable_icons = { opts.drawer.disable_icons, "boolean" }, - drawer_icons = { opts.drawer.icons, "table" }, + drawer_disable_candies = { opts.drawer.disable_candies, "boolean" }, + drawer_candies = { opts.drawer.candies, "table" }, drawer_mappings = { opts.drawer.mappings, "table" }, -- ui ui_window_commands = { opts.ui.window_commands, "table" }, diff --git a/lua/dbee/config.lua b/lua/dbee/config.lua index c5109b9a..88f03c05 100644 --- a/lua/dbee/config.lua +++ b/lua/dbee/config.lua @@ -44,9 +44,9 @@ M.default = { -- loads connections from files and environment variables connection_sources = { -- list of files to load connections from - files = {}, + files = { vim.fn.stdpath("cache") .. "/dbee/persistence.json" }, -- list of env vars to load connections from - env_vars = {}, + env_vars = { "DBEE_CONNECTIONS" }, }, -- extra table helpers per connection type extra_helpers = { @@ -75,36 +75,44 @@ M.default = { toggle = { key = "o", mode = "n" }, }, -- icon settings: - disable_icons = false, - icons = { + disable_candies = false, + candies = { -- these are what's available for now: history = { icon = "", - highlight = "Constant", + icon_highlight = "Constant", }, scratch = { icon = "", - highlight = "Character", + icon_highlight = "Character", }, database = { icon = "", - highlight = "SpecialChar", + icon_highlight = "SpecialChar", }, table = { icon = "", - highlight = "Conditional", + icon_highlight = "Conditional", }, add = { icon = "", - highlight = "String", + icon_highlight = "String", + text_highlight = "String", }, remove = { icon = "󰆴", - highlight = "SpellBad", + icon_highlight = "SpellBad", + text_highlight = "NonText", }, help = { icon = "󰋖", - highlight = "NormalFloat", + icon_highlight = "Title", + text_highlight = "Title", + }, + loader = { + icon = "󰃖", + icon_highlight = "MoreMsg", + text_highlight = "MoreMsg", }, -- if there is no type @@ -115,17 +123,17 @@ M.default = { -- ...and use this for nodes with children none_dir = { icon = "", - highlight = "NonText", + icon_highlight = "NonText", }, -- chevron icons for expanded/closed nodes node_expanded = { icon = "", - highlight = "NonText", + icon_highlight = "NonText", }, node_closed = { icon = "", - highlight = "NonText", + icon_highlight = "NonText", }, }, }, diff --git a/lua/dbee/conn.lua b/lua/dbee/conn.lua index b3aad95e..fbeb977f 100644 --- a/lua/dbee/conn.lua +++ b/lua/dbee/conn.lua @@ -2,7 +2,7 @@ local helpers = require("dbee.helpers") local utils = require("dbee.utils") ---@alias conn_id string ----@alias connection_details { name: string, type: string, url: string, id: conn_id } +---@alias connection_details { name: string, type: string, url: string, id: conn_id, page_size: integer } -- ---@class _LayoutGo ---@field name string display name @@ -18,13 +18,14 @@ local utils = require("dbee.utils") ---@field private id conn_id ---@field private name string ---@field private type string --TODO enum? +---@field private page_size integer ---@field private page_index integer index of the current page ---@field private on_exec fun() callback which gets triggered on any action local Conn = {} ---@param ui Ui ---@param params connection_details ----@param opts? { page_size: integer, on_exec: fun() } +---@param opts? { on_exec: fun() } ---@return Conn function Conn:new(ui, params, opts) params = params or {} @@ -50,14 +51,16 @@ function Conn:new(ui, params, opts) end local type = utils.type_alias(expanded.type) local id = expanded.id or ("__master_connection_id_" .. expanded.name .. expanded.type .. "__") - params.id = id + local page_size = params.page_size or 100 -- register in go - local ok = vim.fn.Dbee_register_connection(id, expanded.url, type, tostring(opts.page_size or 100)) + local ok = vim.fn.Dbee_register_connection(id, expanded.url, type, tostring(page_size)) if not ok then error("problem adding connection") end + params.id = id + -- class object local o = { ui = ui, @@ -65,6 +68,7 @@ function Conn:new(ui, params, opts) id = id, name = name, type = type, + page_size = page_size, page_index = 0, on_exec = opts.on_exec or function() end, } @@ -85,6 +89,7 @@ function Conn:details() -- url shouldn't be seen as expanded - it has secrets url = self.__original.url, type = self.type, + page_size = self.page_size, } end diff --git a/lua/dbee/drawer.lua b/lua/dbee/drawer.lua index b52c5656..09f68a5b 100644 --- a/lua/dbee/drawer.lua +++ b/lua/dbee/drawer.lua @@ -1,26 +1,28 @@ local NuiTree = require("nui.tree") local NuiLine = require("nui.line") ----@class Icon +---@class Candy ---@field icon string ----@field highlight string +---@field icon_highlight string +---@field text_highlight string ---@class Layout ---@field id string unique identifier ---@field name string display name ----@field type ""|"table"|"history"|"scratch"|"database"|"add"|"remove"|"help" type of layout +---@field type ""|"table"|"history"|"scratch"|"database"|"add"|"remove"|"help"|"loader" type of layout ---@field schema? string parent schema ---@field database? string parent database ---@field action_1? fun(cb: fun()) primary action - takes single arg: callback closure ---@field action_2? fun(cb: fun()) secondary action - takes single arg: callback closure ---@field action_3? fun(cb: fun()) tertiary action - takes single arg: callback closure ---@field children? Layout[]|fun():Layout[] child layout nodes +---@field do_expand? boolean expand by default -- node is Layout converted to NuiTreeNode ---@class Node: Layout ---@field getter fun():Layout ----@alias drawer_config { disable_icons: boolean, icons: table, mappings: table } +---@alias drawer_config { disable_candies: boolean, candies: table, mappings: table } ---@class Drawer ---@field private ui Ui @@ -28,7 +30,7 @@ local NuiLine = require("nui.line") ---@field private handler Handler ---@field private editor Editor ---@field private mappings table ----@field private icons table +---@field private candies table map of eye-candy stuff (icons, highlight) local Drawer = {} ---@param ui Ui @@ -49,9 +51,9 @@ function Drawer:new(ui, handler, editor, opts) error("no Editor provided to Drawer") end - local icons = {} - if not opts.disable_icons then - icons = opts.icons or {} + local candies = {} + if not opts.disable_candies then + candies = opts.candies or {} end -- class object @@ -61,10 +63,14 @@ function Drawer:new(ui, handler, editor, opts) handler = handler, editor = editor, mappings = opts.mappings or {}, - icons = icons, + candies = candies, } setmetatable(o, self) self.__index = self + + -- set keymaps + o.ui:set_keymap(o:generate_keymap(opts.mappings)) + return o end @@ -79,38 +85,38 @@ function Drawer:create_tree(bufnr) line:append(string.rep(" ", node:get_depth() - 1)) if node:has_children() or node.getter then - local icon = self.icons["node_closed"] or { icon = ">", highlight = "NonText" } + local candy = self.candies["node_closed"] or { icon = ">", icon_highlight = "NonText" } if node:is_expanded() then - icon = self.icons["node_expanded"] or { icon = "v", highlight = "NonText" } + candy = self.candies["node_expanded"] or { icon = "v", icon_highlight = "NonText" } end - line:append(icon.icon .. " ", icon.highlight) + line:append(candy.icon .. " ", candy.icon_highlight) else line:append(" ") end - ---@type Icon - local icon + ---@type Candy + local candy -- special icons for nodes without type if not node.type or node.type == "" then if node:has_children() then - icon = self.icons["none_dir"] + candy = self.candies["none_dir"] else - icon = self.icons["none"] + candy = self.candies["none"] end else - icon = self.icons[node.type] or {} + candy = self.candies[node.type] or {} end - icon = icon or {} + candy = candy or {} - if icon.icon then - line:append(" " .. icon.icon .. " ", icon.highlight) + if candy.icon then + line:append(" " .. candy.icon .. " ", candy.icon_highlight) end -- if connection is the active one, apply a special highlight on the master if self.handler:current_connection():details().id == node.id then - line:append(node.name, icon.highlight) + line:append(node.name, candy.icon_highlight) else - line:append(node.name) + line:append(node.name, candy.text_highlight) end return line @@ -124,8 +130,12 @@ function Drawer:create_tree(bufnr) } end ----@return table -function Drawer:actions() +---@private +---@param mappings table +---@return keymap[] +function Drawer:generate_keymap(mappings) + mappings = mappings or {} + local function collapse_node(node) if node:collapse() then self.tree:render() @@ -158,76 +168,82 @@ function Drawer:actions() end return { - refresh = function() - self:refresh() - end, - action_1 = function() - local node = self.tree:get_node() - if type(node.action_1) == "function" then - node.action_1(function() - self:refresh() - end) - end - end, - action_2 = function() - local node = self.tree:get_node() - if type(node.action_2) == "function" then - node.action_2(function() - self:refresh() - end) - end - end, - action_3 = function() - local node = self.tree:get_node() - if type(node.action_3) == "function" then - node.action_3(function() - self:refresh() - end) - end - end, - collapse = function() - local node = self.tree:get_node() - if not node then - return - end - collapse_node(node) - end, - expand = function() - local node = self.tree:get_node() - if not node then - return - end - expand_node(node) - end, - toggle = function() - local node = self.tree:get_node() - if not node then - return - end - if node:is_expanded() then + { + action = function() + self:refresh() + end, + mapping = mappings["refresh"] or { key = "r", mode = "n" }, + }, + { + action = function() + local node = self.tree:get_node() + if type(node.action_1) == "function" then + node.action_1(function() + self:refresh() + end) + end + end, + mapping = mappings["action_1"] or { key = "", mode = "n" }, + }, + { + action = function() + local node = self.tree:get_node() + if type(node.action_2) == "function" then + node.action_2(function() + self:refresh() + end) + end + end, + mapping = mappings["action_2"] or { key = "da", mode = "n" }, + }, + { + action = function() + local node = self.tree:get_node() + if type(node.action_3) == "function" then + node.action_3(function() + self:refresh() + end) + end + end, + mapping = mappings["action_3"] or { key = "dd", mode = "n" }, + }, + { + action = function() + local node = self.tree:get_node() + if not node then + return + end collapse_node(node) - else + end, + mapping = mappings["collapse"] or { key = "c", mode = "n" }, + }, + { + action = function() + local node = self.tree:get_node() + if not node then + return + end expand_node(node) - end - end, + end, + mapping = mappings["expand"] or { key = "e", mode = "n" }, + }, + { + action = function() + local node = self.tree:get_node() + if not node then + return + end + if node:is_expanded() then + collapse_node(node) + else + expand_node(node) + end + end, + mapping = mappings["toggle"] or { key = "o", mode = "n" }, + }, } end ----@private ----@param bufnr integer which buffer to map the keys in -function Drawer:map_keys(bufnr) - local map_options = { noremap = true, nowait = true, buffer = bufnr } - - local actions = self:actions() - - for act, map in pairs(self.mappings) do - local action = actions[act] - if action and type(action) == "function" then - vim.keymap.set(map.mode, map.key, action, map_options) - end - end -end - -- sets layout to tree ---@private ---@param layout Layout[] layout to add to tree @@ -265,7 +281,7 @@ function Drawer:set_layout(layout, node_id) -- get existing node from the current tree and check if it is expanded local expanded = false local ex_node = self.tree:get_node(l.id) - if ex_node and ex_node:is_expanded() then + if (ex_node and ex_node:is_expanded()) or l.do_expand then expanded = true -- if getter exists, and node is expanded, we call it if getter then @@ -318,6 +334,7 @@ function Drawer:refresh() id = "__help_layout__", name = "help", type = "help", + do_expand = true, children = help_children, } @@ -349,7 +366,6 @@ function Drawer:open() self:refresh() end - self:map_keys(bufnr) self.tree.bufnr = bufnr self.tree:render() diff --git a/lua/dbee/editor.lua b/lua/dbee/editor.lua index 6facb4f8..f724e589 100644 --- a/lua/dbee/editor.lua +++ b/lua/dbee/editor.lua @@ -9,7 +9,6 @@ local SCRATCHES_DIR = vim.fn.stdpath("cache") .. "/dbee/scratches" ---@class Editor ---@field private ui Ui ---@field private handler Handler ----@field private mappings table ---@field private scratches table id - scratch mapping ---@field private active_scratch scratch_id id of the current scratch local Editor = {} @@ -52,10 +51,13 @@ function Editor:new(ui, handler, opts) handler = handler, scratches = scratches, active_scratch = active, - mappings = opts.mappings or {}, } setmetatable(o, self) self.__index = self + + -- set keymaps + o.ui:set_keymap(o:generate_keymap(opts.mappings)) + return o end @@ -198,41 +200,36 @@ function Editor:set_active_scratch(id) self.active_scratch = id end ----@return table -function Editor:actions() +---@private +---@param mappings table +---@return keymap[] +function Editor:generate_keymap(mappings) + mappings = mappings or {} return { - run_file = function() - local bnr = self.scratches[self.active_scratch].bufnr - local lines = vim.api.nvim_buf_get_lines(bnr, 0, -1, false) - local query = table.concat(lines, "\n") + { + action = function() + local bnr = self.scratches[self.active_scratch].bufnr + local lines = vim.api.nvim_buf_get_lines(bnr, 0, -1, false) + local query = table.concat(lines, "\n") - self.handler:current_connection():execute(query) - end, - run_selection = function() - local srow, scol, erow, ecol = utils.visual_selection() + self.handler:current_connection():execute(query) + end, + mapping = mappings["run_file"] or { key = "BB", mode = "n" }, + }, + { + action = function() + local srow, scol, erow, ecol = utils.visual_selection() - local selection = vim.api.nvim_buf_get_text(0, srow, scol, erow, ecol, {}) - local query = table.concat(selection, "\n") + local selection = vim.api.nvim_buf_get_text(0, srow, scol, erow, ecol, {}) + local query = table.concat(selection, "\n") - self.handler:current_connection():execute(query) - end, + self.handler:current_connection():execute(query) + end, + mapping = mappings["run_selection"] or { key = "BB", mode = "v" }, + }, } end ----@private -function Editor:map_keys(bufnr) - local map_options = { noremap = true, nowait = true, buffer = bufnr } - - local actions = self:actions() - - for act, map in pairs(self.mappings) do - local action = actions[act] - if action and type(action) == "function" then - vim.keymap.set(map.mode, map.key, action, map_options) - end - end -end - function Editor:open() -- each scratchpad is it's own buffer, so we can ignore ui's bufnr local winid, _ = self.ui:open() @@ -280,8 +277,9 @@ function Editor:open() vim.cmd("e " .. s.file) end - -- set keymaps - self:map_keys(bufnr) + -- set keymaps ui's keymaps + self.ui:set_buffer(bufnr) + self.ui:map_keys() self.scratches[self.active_scratch].bufnr = bufnr diff --git a/lua/dbee/handler.lua b/lua/dbee/handler.lua index b04f7a24..a393873b 100644 --- a/lua/dbee/handler.lua +++ b/lua/dbee/handler.lua @@ -2,20 +2,22 @@ local utils = require("dbee.utils") local Conn = require("dbee.conn") local Lookup = require("dbee.lookup") ----@alias loader_id string +---@alias handler_config { expand_help: boolean, default_page_size: integer } -- Handler is an aggregator of connections ---@class Handler ---@field private ui Ui ui for results ---@field private lookup Lookup lookup for loaders and connections ---@field private default_loader_id string +---@field private opts handler_config local Handler = {} ---@param ui Ui ui for displaying results ---@param default_loader Loader ---@param other_loaders? Loader[] +---@param opts? handler_config ---@return Handler -function Handler:new(ui, default_loader, other_loaders) +function Handler:new(ui, default_loader, other_loaders, opts) if not ui then error("no results Ui passed to Handler") end @@ -28,6 +30,7 @@ function Handler:new(ui, default_loader, other_loaders) ui = ui, lookup = Lookup:new(), default_loader_id = default_loader:name(), + opts = opts or {}, } setmetatable(o, self) self.__index = self @@ -35,10 +38,9 @@ function Handler:new(ui, default_loader, other_loaders) -- initialize the default loader and others o:loader_add(default_loader) - if other_loaders then - for _, loader in ipairs(other_loaders) do - pcall(o.loader_add, o, loader) - end + other_loaders = other_loaders or {} + for _, loader in ipairs(other_loaders) do + pcall(o.loader_add, o, loader) end return o @@ -75,10 +77,10 @@ end function Handler:add_connection(params, loader_id) loader_id = loader_id or self.default_loader_id -- create a new connection + params.page_size = params.page_size or self.opts.default_page_size ---@type Conn local conn, ok ok, conn = pcall(Conn.new, Conn, self.ui, params, { - page_size = 100, --[[TODO]] on_exec = function() self:set_active(conn:details().id) end, @@ -114,14 +116,14 @@ function Handler:remove_connection(id) local original_details = conn:original_details() - -- delete it - self.lookup:remove_connection(id) - -- delete it from the loader local loader = self.lookup:get_loaders(id)[1] if loader and type(loader.save) == "function" then loader:save({ original_details }, "delete") end + + -- delete it + self.lookup:remove_connection(id) end ---@param id conn_id connection id @@ -139,53 +141,101 @@ function Handler:layout() ---@type Layout[] local layout = {} - for _, conn in ipairs(self.lookup:get_connections()) do - local details = conn:details() - table.insert(layout, { - id = details.id, - name = details.name, - type = "database", - -- set connection as active manually - action_2 = function(cb) - self:set_active(details.id) - cb() - end, - -- remove connection (also trigger the loader function) - action_3 = function(cb) - vim.ui.input({ prompt = 'confirm deletion of "' .. details.name .. '"', default = "Y" }, function(input) - if not input or string.lower(input) ~= "y" then - return - end - self:remove_connection(details.id) - cb() - end) - end, - children = function() - return conn:layout() - end, - }) - end - - -- add connection dialog - table.insert(layout, { - id = "__add_connection__", - name = "add connection", - type = "add", - action_1 = function(cb) - local prompt = { - "name", - "type", - "url", - } - utils.prompt.open(prompt, { - title = "Add Connection", - callback = function(result) - pcall(self.add_connection, self, result --[[@as connection_details]]) + local all_loaders = self.lookup:get_loaders() + + for _, loader in ipairs(all_loaders) do + local loader_id = loader:name() + + local children = { + { + id = "__loader_add_connection__" .. loader_id, + name = "add", + type = "add", + action_1 = function(cb) + local prompt = { + { name = "name" }, + { name = "type" }, + { name = "url" }, + { name = "page size" }, + } + utils.prompt.open(prompt, { + title = "Add Connection", + callback = function(result) + local spec = { + id = result.id, + name = result.name, + url = result.url, + type = result.type, + page_size = tonumber(result["page size"]), + } + pcall(self.add_connection, self, spec --[[@as connection_details]], loader_id) + cb() + end, + }) + end, + }, + } + + for _, conn in ipairs(self.lookup:get_connections(loader:name())) do + local details = conn:details() + table.insert(children, { + id = details.id, + name = details.name, + type = "database", + -- set connection as active manually + action_1 = function(cb) + self:set_active(details.id) cb() end, + action_2 = function(cb) + local original_details = conn:original_details() + local prompt = { + { name = "name", default = original_details.name }, + { name = "type", default = original_details.type }, + { name = "url", default = original_details.url }, + { name = "page size", default = tostring(original_details.page_size) }, + } + utils.prompt.open(prompt, { + title = "Edit Connection", + callback = function(result) + local spec = { + -- keep the old id + id = original_details.id, + name = result.name, + url = result.url, + type = result.type, + page_size = tonumber(result["page size"]), + } + -- parse page size to int + pcall(self.add_connection, self, spec --[[@as connection_details]], loader_id) + cb() + end, + }) + end, + -- remove connection (also trigger the loader function) + action_3 = function(cb) + vim.ui.input({ prompt = 'confirm deletion of "' .. details.name .. '"', default = "Y" }, function(input) + if not input or string.lower(input) ~= "y" then + return + end + self:remove_connection(details.id) + cb() + end) + end, + children = function() + return conn:layout() + end, }) - end, - }) + end + + table.insert(layout, { + id = "__loader__" .. loader_id, + name = loader_id, + do_expand = (#children > 1) or (#all_loaders == 1), + type = "loader", + children = children, + }) + end return layout end diff --git a/lua/dbee/loader.lua b/lua/dbee/loader.lua index 983ca27c..3af01c73 100644 --- a/lua/dbee/loader.lua +++ b/lua/dbee/loader.lua @@ -1,9 +1,9 @@ local utils = require("dbee.utils") -local DEFAULT_PERSISTENCE_FILE = vim.fn.stdpath("cache") .. "/dbee/persistence.json" - local M = {} +---@alias loader_id string + ---@class Loader ---@field name fun(self: Loader):string function to return the name of the loader ---@field load fun(self: Loader):connection_details[] function to load connections from external source @@ -15,11 +15,14 @@ local M = {} M.FileLoader = {} --- Loads connections from json file ----@param path? string path to file +---@param path string path to file ---@return Loader function M.FileLoader:new(path) + if not path then + error("no path provided") + end local o = { - path = path or DEFAULT_PERSISTENCE_FILE, + path = path, } setmetatable(o, self) self.__index = self @@ -28,7 +31,7 @@ end ---@return string function M.FileLoader:name() - return vim.fs.basename(self.path) + return "FileLoader - " .. vim.fs.basename(self.path) end ---@return connection_details[] @@ -126,11 +129,14 @@ end M.EnvLoader = {} --- Loads connections from json file ----@param var? string env var to load from +---@param var string env var to load from ---@return Loader function M.EnvLoader:new(var) + if not var then + error("no path provided") + end local o = { - var = var or "DBEE_CONNECTIONS", + var = var, } setmetatable(o, self) self.__index = self @@ -139,7 +145,7 @@ end ---@return string function M.EnvLoader:name() - return self.var + return "EnvLoader - " .. self.var end ---@return connection_details[] @@ -186,7 +192,7 @@ end ---@return string function M.MemoryLoader:name() - return "memory" + return "MemoryLoader" end ---@return connection_details[] diff --git a/lua/dbee/lookup.lua b/lua/dbee/lookup.lua index bafe4dda..27a0cc98 100644 --- a/lua/dbee/lookup.lua +++ b/lua/dbee/lookup.lua @@ -63,10 +63,13 @@ function Lookup:add_connection(connection, loader_id) local id = connection:details().id - self:remove_connection(id) + local old = self.connections[id] + if old then + pcall(old.close, old) + end self.connections[id] = connection - self.conn_lookup[loader_id] = id + self.conn_lookup[id] = loader_id table.insert(self.loaders[loader_id].connections, id) self.active_connection = id diff --git a/lua/dbee/result.lua b/lua/dbee/result.lua index 9843424f..af64fd5a 100644 --- a/lua/dbee/result.lua +++ b/lua/dbee/result.lua @@ -6,8 +6,6 @@ ---@class Result ---@field private ui Ui ---@field private handler Handler ----@field private mappings table ----@field private size integer number of rows per page local Result = {} ---@param ui Ui @@ -24,56 +22,43 @@ function Result:new(ui, handler, opts) error("no Ui passed to Result") end - local page_size = opts.page_size or 100 - -- class object local o = { ui = ui, handler = handler, - size = page_size, - mappings = opts.mappings or {}, } setmetatable(o, self) self.__index = self - return o -end ----@return integer # size of one page -function Result:page_size() - return self.size -end + -- set keymaps + o.ui:set_keymap(o:generate_keymap(opts.mappings)) ----@return table -function Result:actions() - return { - page_next = function() - self.handler:current_connection():page_next() - end, - page_prev = function() - self.handler:current_connection():page_prev() - end, - } + return o end ---@private -function Result:map_keys(bufnr) - local map_options = { noremap = true, nowait = true, buffer = bufnr } - - local actions = self:actions() - - for act, map in pairs(self.mappings) do - local action = actions[act] - if action and type(action) == "function" then - vim.keymap.set(map.mode, map.key, action, map_options) - end - end +---@param mappings table +---@return keymap[] +function Result:generate_keymap(mappings) + mappings = mappings or {} + return { + { + action = function() + self.handler:current_connection():page_next() + end, + mapping = mappings["page_next"] or { key = "L", mode = "n" }, + }, + { + action = function() + self.handler:current_connection():page_prev() + end, + mapping = mappings["page_prev"] or { key = "H", mode = "n" }, + }, + } end function Result:open() - local _, bufnr = self.ui:open() - - -- set keymaps - self:map_keys(bufnr) + self.ui:open() end function Result:close() diff --git a/lua/dbee/ui.lua b/lua/dbee/ui.lua index 63ef8c53..f5d473f2 100644 --- a/lua/dbee/ui.lua +++ b/lua/dbee/ui.lua @@ -1,4 +1,5 @@ ---@alias ui_config { buffer_options: table, window_options: table, window_command: string|fun():integer } +---@alias keymap { action: fun(), mapping: mapping } ---@class Ui ---@field private winid integer @@ -6,6 +7,7 @@ ---@field private window_options table ---@field private buffer_options table ---@field private window_command fun():integer function which opens a new window and returns a window id +---@field private keymap keymap[] local Ui = {} ---@param opts? ui_config @@ -35,6 +37,7 @@ function Ui:new(opts) window_command = win_cmd, window_options = opts.window_options or {}, buffer_options = opts.buffer_options or {}, + keymap = {}, } setmetatable(o, self) self.__index = self @@ -51,6 +54,37 @@ function Ui:buffer() return self.bufnr end +---@param keymap keymap[] +function Ui:set_keymap(keymap) + if keymap then + self.keymap = keymap + end +end + +---@param bufnr integer +function Ui:set_buffer(bufnr) + if type(bufnr) == "number" then + self.bufnr = bufnr + end +end + +---@param winid integer +function Ui:set_window(winid) + if type(winid) == "number" then + self.winid = winid + end +end + +function Ui:map_keys() + local map_options = { noremap = true, nowait = true, buffer = self.bufnr } + + for _, m in ipairs(self.keymap) do + if m.action and type(m.action) == "function" and m.mapping and m.mapping.key and m.mapping.mode then + vim.keymap.set(m.mapping.mode, m.mapping.key, m.action, map_options) + end + end +end + ---@return integer winid ---@return integer bufnr function Ui:open() @@ -74,6 +108,8 @@ function Ui:open() vim.api.nvim_win_set_option(self.winid, opt, val) end + self:map_keys() + return self.winid, self.bufnr end diff --git a/lua/dbee/utils/prompt.lua b/lua/dbee/utils/prompt.lua index 47781fcc..2e9c8572 100644 --- a/lua/dbee/utils/prompt.lua +++ b/lua/dbee/utils/prompt.lua @@ -1,19 +1,19 @@ local M = {} ----@param prompt string[] list of lines to display as prompt +---@param prompt { name: string, default: string }[] list of lines with optional defaults to display as prompt ---@param opts? { width: integer, height: integer, title: string, border: string|string[], callback: fun(result: table) } optional parameters function M.open(prompt, opts) opts = opts or {} - -- add colons to prompt - for i, p in ipairs(prompt) do - if not p:find(":$") then - prompt[i] = p .. ":" - end + -- create lines to display + ---@type string[] + local display_prompt = {} + for _, p in ipairs(prompt) do + table.insert(display_prompt, p.name .. ": " .. (p.default or "")) end local win_width = opts.width or 100 - local win_height = opts.height or #prompt + local win_height = opts.height or #display_prompt local ui_spec = vim.api.nvim_list_uis()[1] local x = math.floor((ui_spec["width"] - win_width) / 2) local y = math.floor((ui_spec["height"] - win_height) / 2) @@ -27,7 +27,7 @@ function M.open(prompt, opts) vim.api.nvim_buf_set_option(bufnr, "bufhidden", "delete") -- fill buffer contents - vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, prompt) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, display_prompt) vim.api.nvim_buf_set_option(bufnr, "modified", false) -- open window @@ -66,13 +66,13 @@ function M.open(prompt, opts) local kv = {} for _, p in ipairs(prompt) do -- get key from prompt and store it as empty string by default - local key = p:gsub("(.*):", "%1") + local key = p.name kv[key] = "" for _, l in ipairs(lines) do -- if line has prompt prefix, get the value and strip whitespace - if l:find("^%s*" .. p) then - local val = l:gsub("^%s*" .. p .. "%s*(.-)%s*$", "%1") + if l:find("^%s*" .. p.name .. ":") then + local val = l:gsub("^%s*" .. p.name .. ":%s*(.-)%s*$", "%1") kv[key] = val end end From 9f48f650074c67de2877a412e2cf3ead9cc2a4f6 Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Sat, 20 May 2023 20:11:18 +0200 Subject: [PATCH 17/25] - added edit source option to connection sources - removed default keymaps from components --- lua/dbee/config.lua | 13 ++++--- lua/dbee/drawer.lua | 16 ++++----- lua/dbee/editor.lua | 4 +-- lua/dbee/handler.lua | 73 ++++++++++++++++++++++++++++++--------- lua/dbee/loader.lua | 14 +++++--- lua/dbee/result.lua | 4 +-- lua/dbee/utils/prompt.lua | 54 +++++++++++++++++++++++++++++ 7 files changed, 142 insertions(+), 36 deletions(-) diff --git a/lua/dbee/config.lua b/lua/dbee/config.lua index 88f03c05..dc5f98a8 100644 --- a/lua/dbee/config.lua +++ b/lua/dbee/config.lua @@ -66,12 +66,12 @@ M.default = { -- action_1 opens a scratchpad or executes a helper action_1 = { key = "", mode = "n" }, -- action_2 renames a scratchpad or sets the connection as active manually - action_2 = { key = "da", mode = "n" }, + action_2 = { key = "cw", mode = "n" }, -- action_3 deletes a scratchpad or connection (removes connection from the file if you configured it like so) action_3 = { key = "dd", mode = "n" }, -- these are self-explanatory: - collapse = { key = "c", mode = "n" }, - expand = { key = "e", mode = "n" }, + -- collapse = { key = "c", mode = "n" }, + -- expand = { key = "e", mode = "n" }, toggle = { key = "o", mode = "n" }, }, -- icon settings: @@ -99,10 +99,15 @@ M.default = { icon_highlight = "String", text_highlight = "String", }, + edit = { + icon = "󰏫", + icon_highlight = "Directory", + text_highlight = "Directory", + }, remove = { icon = "󰆴", icon_highlight = "SpellBad", - text_highlight = "NonText", + text_highlight = "SpellBad", }, help = { icon = "󰋖", diff --git a/lua/dbee/drawer.lua b/lua/dbee/drawer.lua index 09f68a5b..211f4c5e 100644 --- a/lua/dbee/drawer.lua +++ b/lua/dbee/drawer.lua @@ -9,7 +9,7 @@ local NuiLine = require("nui.line") ---@class Layout ---@field id string unique identifier ---@field name string display name ----@field type ""|"table"|"history"|"scratch"|"database"|"add"|"remove"|"help"|"loader" type of layout +---@field type ""|"table"|"history"|"scratch"|"database"|"add"|"edit"|"remove"|"help"|"loader" type of layout ---@field schema? string parent schema ---@field database? string parent database ---@field action_1? fun(cb: fun()) primary action - takes single arg: callback closure @@ -172,7 +172,7 @@ function Drawer:generate_keymap(mappings) action = function() self:refresh() end, - mapping = mappings["refresh"] or { key = "r", mode = "n" }, + mapping = mappings["refresh"], }, { action = function() @@ -183,7 +183,7 @@ function Drawer:generate_keymap(mappings) end) end end, - mapping = mappings["action_1"] or { key = "", mode = "n" }, + mapping = mappings["action_1"], }, { action = function() @@ -194,7 +194,7 @@ function Drawer:generate_keymap(mappings) end) end end, - mapping = mappings["action_2"] or { key = "da", mode = "n" }, + mapping = mappings["action_2"], }, { action = function() @@ -205,7 +205,7 @@ function Drawer:generate_keymap(mappings) end) end end, - mapping = mappings["action_3"] or { key = "dd", mode = "n" }, + mapping = mappings["action_3"], }, { action = function() @@ -215,7 +215,7 @@ function Drawer:generate_keymap(mappings) end collapse_node(node) end, - mapping = mappings["collapse"] or { key = "c", mode = "n" }, + mapping = mappings["collapse"], }, { action = function() @@ -225,7 +225,7 @@ function Drawer:generate_keymap(mappings) end expand_node(node) end, - mapping = mappings["expand"] or { key = "e", mode = "n" }, + mapping = mappings["expand"], }, { action = function() @@ -239,7 +239,7 @@ function Drawer:generate_keymap(mappings) expand_node(node) end end, - mapping = mappings["toggle"] or { key = "o", mode = "n" }, + mapping = mappings["toggle"], }, } end diff --git a/lua/dbee/editor.lua b/lua/dbee/editor.lua index f724e589..b9672e73 100644 --- a/lua/dbee/editor.lua +++ b/lua/dbee/editor.lua @@ -214,7 +214,7 @@ function Editor:generate_keymap(mappings) self.handler:current_connection():execute(query) end, - mapping = mappings["run_file"] or { key = "BB", mode = "n" }, + mapping = mappings["run_file"], }, { action = function() @@ -225,7 +225,7 @@ function Editor:generate_keymap(mappings) self.handler:current_connection():execute(query) end, - mapping = mappings["run_selection"] or { key = "BB", mode = "v" }, + mapping = mappings["run_selection"], }, } end diff --git a/lua/dbee/handler.lua b/lua/dbee/handler.lua index a393873b..fad84041 100644 --- a/lua/dbee/handler.lua +++ b/lua/dbee/handler.lua @@ -63,10 +63,29 @@ function Handler:loader_reload(id) return end - local specs = loader:load() + -- remove old connections + local old_conns = self.lookup:get_connections(id) + for _, conn in ipairs(old_conns) do + self.lookup:remove_connection(conn:details().id) + end - for _, spec in ipairs(specs) do - self:add_connection(spec, id) + -- add new connections + for _, spec in ipairs(loader:load()) do + -- create a new connection + spec.page_size = spec.page_size or self.opts.default_page_size + ---@type Conn + local conn, ok + ok, conn = pcall(Conn.new, Conn, self.ui, spec, { + on_exec = function() + self:set_active(conn:details().id) + end, + }) + if ok then + -- add it to lookup + self.lookup:add_connection(conn, id) + else + utils.log("error", tostring(conn), "handler") + end end end @@ -146,8 +165,11 @@ function Handler:layout() for _, loader in ipairs(all_loaders) do local loader_id = loader:name() - local children = { - { + local children = {} + + -- loader can save edits + if type(loader.save) == "function" or loader_id == self.default_loader_id then + table.insert(children, { id = "__loader_add_connection__" .. loader_id, name = "add", type = "add", @@ -173,10 +195,27 @@ function Handler:layout() end, }) end, - }, - } + }) + end + -- loader has an editable source + if type(loader.source) == "function" then + table.insert(children, { + id = "__loader_edit_connections__" .. loader_id, + name = "edit source", + type = "edit", + action_1 = function(cb) + utils.prompt.edit(loader:source(), { + title = "Add Connection", + callback = function() + self:loader_reload(loader_id) + cb() + end, + }) + end, + }) + end - for _, conn in ipairs(self.lookup:get_connections(loader:name())) do + for _, conn in ipairs(self.lookup:get_connections(loader_id)) do local details = conn:details() table.insert(children, { id = details.id, @@ -193,7 +232,7 @@ function Handler:layout() { name = "name", default = original_details.name }, { name = "type", default = original_details.type }, { name = "url", default = original_details.url }, - { name = "page size", default = tostring(original_details.page_size) }, + { name = "page size", default = tostring(original_details.page_size or "") }, } utils.prompt.open(prompt, { title = "Edit Connection", @@ -228,13 +267,15 @@ function Handler:layout() }) end - table.insert(layout, { - id = "__loader__" .. loader_id, - name = loader_id, - do_expand = (#children > 1) or (#all_loaders == 1), - type = "loader", - children = children, - }) + if #children > 0 then + table.insert(layout, { + id = "__loader__" .. loader_id, + name = loader_id, + do_expand = true, + type = "loader", + children = children, + }) + end end return layout diff --git a/lua/dbee/loader.lua b/lua/dbee/loader.lua index 3af01c73..4cd9e432 100644 --- a/lua/dbee/loader.lua +++ b/lua/dbee/loader.lua @@ -8,6 +8,7 @@ local M = {} ---@field name fun(self: Loader):string function to return the name of the loader ---@field load fun(self: Loader):connection_details[] function to load connections from external source ---@field save? fun(self: Loader, conns: connection_details[], action: "add"|"delete") function to save connections to external source (optional) +---@field source? fun(self: Loader):string function which returns a source file to edit (optional) --- File loader ---@class FileLoader: Loader @@ -31,7 +32,7 @@ end ---@return string function M.FileLoader:name() - return "FileLoader - " .. vim.fs.basename(self.path) + return vim.fs.basename(self.path) end ---@return connection_details[] @@ -60,7 +61,7 @@ function M.FileLoader:load() end for _, conn in pairs(data) do - if type(conn) == "table" and conn.url and conn.type then + if type(conn) == "table" then table.insert(conns, conn) end end @@ -123,6 +124,11 @@ function M.FileLoader:save(conns, action) file:close() end +---@return string +function M.FileLoader:source() + return self.path +end + --- Environment loader ---@class EnvLoader: Loader ---@field private var string path to file @@ -145,7 +151,7 @@ end ---@return string function M.EnvLoader:name() - return "EnvLoader - " .. self.var + return self.var end ---@return connection_details[] @@ -192,7 +198,7 @@ end ---@return string function M.MemoryLoader:name() - return "MemoryLoader" + return "memory" end ---@return connection_details[] diff --git a/lua/dbee/result.lua b/lua/dbee/result.lua index af64fd5a..70a412b8 100644 --- a/lua/dbee/result.lua +++ b/lua/dbee/result.lua @@ -46,13 +46,13 @@ function Result:generate_keymap(mappings) action = function() self.handler:current_connection():page_next() end, - mapping = mappings["page_next"] or { key = "L", mode = "n" }, + mapping = mappings["page_next"] }, { action = function() self.handler:current_connection():page_prev() end, - mapping = mappings["page_prev"] or { key = "H", mode = "n" }, + mapping = mappings["page_prev"] }, } end diff --git a/lua/dbee/utils/prompt.lua b/lua/dbee/utils/prompt.lua index 2e9c8572..270e9a55 100644 --- a/lua/dbee/utils/prompt.lua +++ b/lua/dbee/utils/prompt.lua @@ -101,4 +101,58 @@ function M.open(prompt, opts) end, { silent = true, buffer = bufnr }) end +---@param file string file to edit +---@param opts? { width: integer, height: integer, title: string, border: string|string[], callback: fun() } optional parameters +function M.edit(file, opts) + opts = opts or {} + + local ui_spec = vim.api.nvim_list_uis()[1] + local win_width = opts.width or (ui_spec["width"] - 50) + local win_height = opts.height or (ui_spec["height"] - 10) + local x = math.floor((ui_spec["width"] - win_width) / 2) + local y = math.floor((ui_spec["height"] - win_height) / 2) + + -- create new dummy buffer + local tmp_buf = vim.api.nvim_create_buf(false, true) + + -- open window + local winid = vim.api.nvim_open_win(tmp_buf, true, { + relative = "editor", + width = win_width, + height = win_height, + col = x, + row = y, + border = opts.border or "rounded", + title = opts.title or "", + title_pos = "center", + style = "minimal", + }) + + -- open the file + vim.cmd("e " .. file) + local bufnr = vim.api.nvim_get_current_buf() + vim.api.nvim_buf_set_option(bufnr, "bufhidden", "delete") + + local callback = opts.callback or function() end + + -- set callbacks + vim.api.nvim_create_autocmd("BufWritePost", { + buffer = bufnr, + callback = callback, + }) + + vim.api.nvim_create_autocmd({ "BufLeave", "BufWritePost" }, { + buffer = bufnr, + callback = function() + pcall(vim.api.nvim_win_close, winid, true) + pcall(vim.api.nvim_buf_delete, bufnr, {}) + end, + }) + + -- set keymaps + vim.keymap.set("n", "q", function() + vim.api.nvim_win_close(winid, true) + end, { silent = true, buffer = bufnr }) +end + return M From c5ada0b931cf11dca22a69f9236293e0348c63c1 Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Sat, 20 May 2023 20:27:13 +0200 Subject: [PATCH 18/25] moved connections and lookup to handler --- lua/dbee/{ => handler}/conn.lua | 0 lua/dbee/{handler.lua => handler/init.lua} | 4 ++-- lua/dbee/{ => handler}/lookup.lua | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename lua/dbee/{ => handler}/conn.lua (100%) rename lua/dbee/{handler.lua => handler/init.lua} (98%) rename lua/dbee/{ => handler}/lookup.lua (100%) diff --git a/lua/dbee/conn.lua b/lua/dbee/handler/conn.lua similarity index 100% rename from lua/dbee/conn.lua rename to lua/dbee/handler/conn.lua diff --git a/lua/dbee/handler.lua b/lua/dbee/handler/init.lua similarity index 98% rename from lua/dbee/handler.lua rename to lua/dbee/handler/init.lua index fad84041..dc889ce2 100644 --- a/lua/dbee/handler.lua +++ b/lua/dbee/handler/init.lua @@ -1,6 +1,6 @@ local utils = require("dbee.utils") -local Conn = require("dbee.conn") -local Lookup = require("dbee.lookup") +local Conn = require("dbee.handler.conn") +local Lookup = require("dbee.handler.lookup") ---@alias handler_config { expand_help: boolean, default_page_size: integer } diff --git a/lua/dbee/lookup.lua b/lua/dbee/handler/lookup.lua similarity index 100% rename from lua/dbee/lookup.lua rename to lua/dbee/handler/lookup.lua From c0606cb20ac08c72f23cd00ea7956959863167d3 Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Sat, 20 May 2023 21:01:26 +0200 Subject: [PATCH 19/25] moved helpers to handler and implemented them as a class --- lua/dbee.lua | 3 +- lua/dbee/handler/conn.lua | 26 ++++----- lua/dbee/{ => handler}/helpers.lua | 90 ++++++++++++++++++++---------- lua/dbee/handler/init.lua | 21 ++++++- lua/dbee/utils/init.lua | 12 ++++ 5 files changed, 101 insertions(+), 51 deletions(-) rename lua/dbee/{ => handler}/helpers.lua (67%) diff --git a/lua/dbee.lua b/lua/dbee.lua index 669c0567..8eea8773 100644 --- a/lua/dbee.lua +++ b/lua/dbee.lua @@ -9,7 +9,6 @@ local FileLoader = require("dbee.loader").FileLoader local install = require("dbee.install") local utils = require("dbee.utils") local default_config = require("dbee.config").default -local helpers = require("dbee.helpers") -- public and private module objects local M = {} @@ -73,7 +72,7 @@ local function lazy_setup() m.editor = Editor:new(editor_ui, m.handler, m.config.editor) m.drawer = Drawer:new(drawer_ui, m.handler, m.editor, m.config.drawer) - helpers.add(m.config.extra_helpers) + m.handler:add_helpers(m.config.extra_helpers) end ---@return boolean ok was setup successful? diff --git a/lua/dbee/handler/conn.lua b/lua/dbee/handler/conn.lua index fbeb977f..43be635b 100644 --- a/lua/dbee/handler/conn.lua +++ b/lua/dbee/handler/conn.lua @@ -1,4 +1,3 @@ -local helpers = require("dbee.helpers") local utils = require("dbee.utils") ---@alias conn_id string @@ -14,6 +13,7 @@ local utils = require("dbee.utils") -- Conn is a 1:1 mapping to go's connections ---@class Conn ---@field private ui Ui +---@field private helpers Helpers ---@field private __original connection_details original unmodified fields passed on initialization (params) ---@field private id conn_id ---@field private name string @@ -24,10 +24,11 @@ local utils = require("dbee.utils") local Conn = {} ---@param ui Ui +---@param helpers Helpers ---@param params connection_details ---@param opts? { on_exec: fun() } ---@return Conn -function Conn:new(ui, params, opts) +function Conn:new(ui, helpers, params, opts) params = params or {} opts = opts or {} @@ -37,6 +38,9 @@ function Conn:new(ui, params, opts) if not ui then error("no Ui provided to Conn!") end + if not helpers then + error("no Helpers provided to Conn!") + end if not expanded.url then error("url needs to be set!") end @@ -64,6 +68,7 @@ function Conn:new(ui, params, opts) -- class object local o = { ui = ui, + helpers = helpers, __original = params, id = id, name = name, @@ -174,23 +179,12 @@ function Conn:layout() local action_1 if lgo.type == "table" then action_1 = function(cb) - local table_helpers = helpers.get(self.type) - local helper_keys = {} - for k, _ in pairs(table_helpers) do - table.insert(helper_keys, k) - end - table.sort(helper_keys) - -- select a helper to execute - vim.ui.select(helper_keys, { + local helpers = self.helpers:get(self.type, { table = lgo.name, schema = lgo.schema, dbname = lgo.database }) + vim.ui.select(utils.sorted_keys(helpers), { prompt = "select a helper to execute:", }, function(selection) if selection then - self:execute( - helpers.expand_query( - table_helpers[selection], - { table = lgo.name, schema = lgo.schema, dbname = lgo.database } - ) - ) + self:execute(helpers[selection]) end cb() end) diff --git a/lua/dbee/helpers.lua b/lua/dbee/handler/helpers.lua similarity index 67% rename from lua/dbee/helpers.lua rename to lua/dbee/handler/helpers.lua index bce89e54..514c88e4 100644 --- a/lua/dbee/helpers.lua +++ b/lua/dbee/handler/helpers.lua @@ -1,15 +1,27 @@ local utils = require("dbee.utils") -local M = {} +---@alias table_helpers table --- extra helpers per type ----@type table -local extras = {} +---@class Helpers +---@field private extras table extra table helpers per type +local Helpers = {} ----@alias table_helpers table +---@param opts? { extras: table } +---@return Helpers +function Helpers:new(opts) + opts = opts or {} + + local o = { + extras = opts.extras or {}, + } + setmetatable(o, self) + self.__index = self + return o +end +---@private ---@return table_helpers helpers list of table helpers -local function postgres() +function Helpers:__postgres() local basic_constraint_query = [[ SELECT tc.constraint_name, tc.table_name, kcu.column_name, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name, rc.update_rule, rc.delete_rule FROM @@ -34,8 +46,9 @@ local function postgres() } end +---@private ---@return table_helpers helpers list of table helpers -local function mysql() +function Helpers:__mysql() return { List = "SELECT * from `{table}` LIMIT 500", Columns = "DESCRIBE `{table}`", @@ -45,8 +58,9 @@ local function mysql() } end +---@private ---@return table_helpers helpers list of table helpers -local function sqlite() +function Helpers:__sqlite() return { List = 'select * from "{table}" LIMIT 500', Indexes = "SELECT * FROM pragma_index_list('{table}')", @@ -55,64 +69,80 @@ local function sqlite() } end +---@private ---@return table_helpers helpers list of table helpers -local function redis() +function Helpers:__redis() return { List = "KEYS *", } end +---@private ---@return table_helpers helpers list of table helpers -local function mongo() +function Helpers:__mongo() return { List = '{"find": "{table}"}', } end ---@param type string +---@param vars { table: string, schema: string, dbname: string } ---@return table_helpers helpers list of table helpers -function M.get(type) - local hs +function Helpers:get(type, vars) + local helpers if type == "postgres" then - hs = postgres() + helpers = self:__postgres() elseif type == "mysql" then - hs = mysql() + helpers = self:__mysql() elseif type == "sqlite" then - hs = sqlite() + helpers = self:__sqlite() elseif type == "redis" then - hs = redis() + helpers = self:__redis() elseif type == "mongo" then - hs = mongo() + helpers = self:__mongo() end - if not hs then + if not helpers then error("unsupported table type for helpers: " .. type) end -- apply extras - local ex = extras[type] or {} + local ex = self.extras[type] or {} + helpers = vim.tbl_deep_extend("force", helpers, ex) - return vim.tbl_deep_extend("force", hs, ex) + return self:expand(helpers, vars or {}) --[[@as table_helpers]] end ----@param unexpanded_query string +---@private +---@param obj string|table_helpers ---@param vars { table: string, schema: string, dbname: string } ----@return string query with expanded vars -function M.expand_query(unexpanded_query, vars) - local ret = unexpanded_query - for key, val in pairs(vars) do - ret = ret:gsub("{" .. key .. "}", val) +---@return string|table_helpers # returns depending on what's passed in +function Helpers:expand(obj, vars) + local function exp(o) + if type(o) ~= "string" then + return o + end + local ret = o + for key, val in pairs(vars) do + ret = ret:gsub("{" .. key .. "}", val) + end + return ret + end + + if type(obj) == "table" then + return vim.tbl_map(exp, obj) end - return ret + + return exp(obj) end ---@param helpers table extra helpers to add (per type) -function M.add(helpers) +function Helpers:add(helpers) local ext = {} for t, hs in pairs(helpers) do ext[utils.type_alias(t)] = hs end - extras = vim.tbl_deep_extend("force", extras, ext) + self.extras = vim.tbl_deep_extend("force", self.extras, ext) end -return M +return Helpers diff --git a/lua/dbee/handler/init.lua b/lua/dbee/handler/init.lua index dc889ce2..1a35880e 100644 --- a/lua/dbee/handler/init.lua +++ b/lua/dbee/handler/init.lua @@ -1,13 +1,15 @@ local utils = require("dbee.utils") local Conn = require("dbee.handler.conn") +local Helpers = require("dbee.handler.helpers") local Lookup = require("dbee.handler.lookup") ----@alias handler_config { expand_help: boolean, default_page_size: integer } +---@alias handler_config { default_page_size: integer } -- Handler is an aggregator of connections ---@class Handler ---@field private ui Ui ui for results ---@field private lookup Lookup lookup for loaders and connections +---@field private helpers Helpers query helpers ---@field private default_loader_id string ---@field private opts handler_config local Handler = {} @@ -29,6 +31,7 @@ function Handler:new(ui, default_loader, other_loaders, opts) local o = { ui = ui, lookup = Lookup:new(), + helpers = Helpers:new(), default_loader_id = default_loader:name(), opts = opts or {}, } @@ -75,7 +78,7 @@ function Handler:loader_reload(id) spec.page_size = spec.page_size or self.opts.default_page_size ---@type Conn local conn, ok - ok, conn = pcall(Conn.new, Conn, self.ui, spec, { + ok, conn = pcall(Conn.new, Conn, self.ui, self.helpers, spec, { on_exec = function() self:set_active(conn:details().id) end, @@ -99,7 +102,7 @@ function Handler:add_connection(params, loader_id) params.page_size = params.page_size or self.opts.default_page_size ---@type Conn local conn, ok - ok, conn = pcall(Conn.new, Conn, self.ui, params, { + ok, conn = pcall(Conn.new, Conn, self.ui, self.helpers, params, { on_exec = function() self:set_active(conn:details().id) end, @@ -155,6 +158,18 @@ function Handler:current_connection() return self.lookup:get_active_connection() end +---@param helpers table extra helpers per type +function Handler:add_helpers(helpers) + self.helpers:add(helpers) +end + +---@param type string +---@param vars { table: string, schema: string, dbname: string } +---@return table_helpers helpers list of table helpers +function Handler:get_helpers(type, vars) + return self.helpers:get(type, vars) +end + ---@return Layout[] function Handler:layout() ---@type Layout[] diff --git a/lua/dbee/utils/init.lua b/lua/dbee/utils/init.lua index 48a43878..41eadf16 100644 --- a/lua/dbee/utils/init.lua +++ b/lua/dbee/utils/init.lua @@ -96,4 +96,16 @@ function M.expand_environment(obj) return expand(obj) end +-- Gets keys of a map and sorts them by name +---@param obj table map-like table +---@return string[] +function M.sorted_keys(obj) + local keys = {} + for k, _ in pairs(obj) do + table.insert(keys, k) + end + table.sort(keys) + return keys +end + return M From c023f262e9a5cde2c189fa8c3c695ea5d5e5878f Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Sat, 20 May 2023 21:17:48 +0200 Subject: [PATCH 20/25] renamed loaders to sources --- lua/dbee.lua | 20 ++--- lua/dbee/config.lua | 2 +- lua/dbee/drawer.lua | 2 +- lua/dbee/handler/init.lua | 108 +++++++++++++-------------- lua/dbee/handler/lookup.lua | 94 +++++++++++------------ lua/dbee/{loader.lua => sources.lua} | 61 +++++++-------- 6 files changed, 142 insertions(+), 145 deletions(-) rename lua/dbee/{loader.lua => sources.lua} (77%) diff --git a/lua/dbee.lua b/lua/dbee.lua index 8eea8773..8d170ed4 100644 --- a/lua/dbee.lua +++ b/lua/dbee.lua @@ -3,9 +3,9 @@ local Editor = require("dbee.editor") local Result = require("dbee.result") local Ui = require("dbee.ui") local Handler = require("dbee.handler") -local MemoryLoader = require("dbee.loader").MemoryLoader -local EnvLoader = require("dbee.loader").EnvLoader -local FileLoader = require("dbee.loader").FileLoader +local MemorySource = require("dbee.sources").MemorySource +local EnvSource = require("dbee.sources").EnvSource +local FileSource = require("dbee.sources").FileSource local install = require("dbee.install") local utils = require("dbee.utils") local default_config = require("dbee.config").default @@ -54,20 +54,20 @@ local function lazy_setup() }, } - -- handler and loaders - -- memory loader loads configs from setup() and is also a default loader - local mem_loader = MemoryLoader:new(m.config.connections) + -- handler and sources + -- memory source loads configs from setup() and is also a default source + local mem_source = MemorySource:new(m.config.connections) - local loaders = {} + local sources = {} for _, file in ipairs(m.config.connection_sources.files) do - table.insert(loaders, FileLoader:new(file)) + table.insert(sources, FileSource:new(file)) end for _, var in ipairs(m.config.connection_sources.env_vars) do - table.insert(loaders, EnvLoader:new(var)) + table.insert(sources, EnvSource:new(var)) end -- set up modules - m.handler = Handler:new(result_ui, mem_loader, loaders) + m.handler = Handler:new(result_ui, mem_source, sources) m.result = Result:new(result_ui, m.handler, m.config.result) m.editor = Editor:new(editor_ui, m.handler, m.config.editor) m.drawer = Drawer:new(drawer_ui, m.handler, m.editor, m.config.drawer) diff --git a/lua/dbee/config.lua b/lua/dbee/config.lua index dc5f98a8..5fcb2150 100644 --- a/lua/dbee/config.lua +++ b/lua/dbee/config.lua @@ -114,7 +114,7 @@ M.default = { icon_highlight = "Title", text_highlight = "Title", }, - loader = { + source = { icon = "󰃖", icon_highlight = "MoreMsg", text_highlight = "MoreMsg", diff --git a/lua/dbee/drawer.lua b/lua/dbee/drawer.lua index 211f4c5e..1d954de4 100644 --- a/lua/dbee/drawer.lua +++ b/lua/dbee/drawer.lua @@ -9,7 +9,7 @@ local NuiLine = require("nui.line") ---@class Layout ---@field id string unique identifier ---@field name string display name ----@field type ""|"table"|"history"|"scratch"|"database"|"add"|"edit"|"remove"|"help"|"loader" type of layout +---@field type ""|"table"|"history"|"scratch"|"database"|"add"|"edit"|"remove"|"help"|"source" type of layout ---@field schema? string parent schema ---@field database? string parent database ---@field action_1? fun(cb: fun()) primary action - takes single arg: callback closure diff --git a/lua/dbee/handler/init.lua b/lua/dbee/handler/init.lua index 1a35880e..7ba84102 100644 --- a/lua/dbee/handler/init.lua +++ b/lua/dbee/handler/init.lua @@ -8,23 +8,23 @@ local Lookup = require("dbee.handler.lookup") -- Handler is an aggregator of connections ---@class Handler ---@field private ui Ui ui for results ----@field private lookup Lookup lookup for loaders and connections +---@field private lookup Lookup lookup for sources and connections ---@field private helpers Helpers query helpers ----@field private default_loader_id string +---@field private default_source_id string ---@field private opts handler_config local Handler = {} ---@param ui Ui ui for displaying results ----@param default_loader Loader ----@param other_loaders? Loader[] +---@param default_source Source +---@param other_sources? Source[] ---@param opts? handler_config ---@return Handler -function Handler:new(ui, default_loader, other_loaders, opts) +function Handler:new(ui, default_source, other_sources, opts) if not ui then error("no results Ui passed to Handler") end - if not default_loader then - error("no default Loader passed to Handler") + if not default_source then + error("no default Source passed to Handler") end -- class object @@ -32,37 +32,37 @@ function Handler:new(ui, default_loader, other_loaders, opts) ui = ui, lookup = Lookup:new(), helpers = Helpers:new(), - default_loader_id = default_loader:name(), + default_source_id = default_source:name(), opts = opts or {}, } setmetatable(o, self) self.__index = self - -- initialize the default loader and others - o:loader_add(default_loader) + -- initialize the default source and others + o:source_add(default_source) - other_loaders = other_loaders or {} - for _, loader in ipairs(other_loaders) do - pcall(o.loader_add, o, loader) + other_sources = other_sources or {} + for _, source in ipairs(other_sources) do + pcall(o.source_add, o, source) end return o end -- add new source and load connections from it ----@param loader Loader -function Handler:loader_add(loader) - local id = loader:name() +---@param source Source +function Handler:source_add(source) + local id = source:name() -- add it - self.lookup:add_loader(loader) + self.lookup:add_source(source) -- and load it's connections - self:loader_reload(id) + self:source_reload(id) end ----@param id loader_id -function Handler:loader_reload(id) - local loader = self.lookup:get_loader(id) - if not loader then +---@param id source_id +function Handler:source_reload(id) + local source = self.lookup:get_source(id) + if not source then return end @@ -73,7 +73,7 @@ function Handler:loader_reload(id) end -- add new connections - for _, spec in ipairs(loader:load()) do + for _, spec in ipairs(source:load()) do -- create a new connection spec.page_size = spec.page_size or self.opts.default_page_size ---@type Conn @@ -94,10 +94,10 @@ end --- adds connection ---@param params connection_details ----@param loader_id? loader_id id of the loader to save connection to +---@param source_id? source_id id of the source to save connection to ---@return conn_id # id of the added connection -function Handler:add_connection(params, loader_id) - loader_id = loader_id or self.default_loader_id +function Handler:add_connection(params, source_id) + source_id = source_id or self.default_source_id -- create a new connection params.page_size = params.page_size or self.opts.default_page_size ---@type Conn @@ -116,19 +116,19 @@ function Handler:add_connection(params, loader_id) self:remove_connection(conn:details().id) -- add it to lookup - self.lookup:add_connection(conn, loader_id) + self.lookup:add_connection(conn, source_id) - -- save it to loader if it exists - local loader = self.lookup:get_loader(loader_id) - if loader and type(loader.save) == "function" then - loader:save({ conn:original_details() }, "add") + -- save it to source if it exists + local source = self.lookup:get_source(source_id) + if source and type(source.save) == "function" then + source:save({ conn:original_details() }, "add") end return conn:details().id end -- removes/unregisters connection --- also deletes it from the loader if it exists +-- also deletes it from the source if it exists ---@param id conn_id connection id function Handler:remove_connection(id) local conn = self.lookup:get_connection(id) @@ -138,10 +138,10 @@ function Handler:remove_connection(id) local original_details = conn:original_details() - -- delete it from the loader - local loader = self.lookup:get_loaders(id)[1] - if loader and type(loader.save) == "function" then - loader:save({ original_details }, "delete") + -- delete it from the source + local source = self.lookup:get_sources(id)[1] + if source and type(source.save) == "function" then + source:save({ original_details }, "delete") end -- delete it @@ -175,17 +175,17 @@ function Handler:layout() ---@type Layout[] local layout = {} - local all_loaders = self.lookup:get_loaders() + local all_sources = self.lookup:get_sources() - for _, loader in ipairs(all_loaders) do - local loader_id = loader:name() + for _, source in ipairs(all_sources) do + local source_id = source:name() local children = {} - -- loader can save edits - if type(loader.save) == "function" or loader_id == self.default_loader_id then + -- source can save edits + if type(source.save) == "function" or source_id == self.default_source_id then table.insert(children, { - id = "__loader_add_connection__" .. loader_id, + id = "__source_add_connection__" .. source_id, name = "add", type = "add", action_1 = function(cb) @@ -205,24 +205,24 @@ function Handler:layout() type = result.type, page_size = tonumber(result["page size"]), } - pcall(self.add_connection, self, spec --[[@as connection_details]], loader_id) + pcall(self.add_connection, self, spec --[[@as connection_details]], source_id) cb() end, }) end, }) end - -- loader has an editable source - if type(loader.source) == "function" then + -- source has an editable source + if type(source.file) == "function" then table.insert(children, { - id = "__loader_edit_connections__" .. loader_id, + id = "__source_edit_connections__" .. source_id, name = "edit source", type = "edit", action_1 = function(cb) - utils.prompt.edit(loader:source(), { + utils.prompt.edit(source:file(), { title = "Add Connection", callback = function() - self:loader_reload(loader_id) + self:source_reload(source_id) cb() end, }) @@ -230,7 +230,7 @@ function Handler:layout() }) end - for _, conn in ipairs(self.lookup:get_connections(loader_id)) do + for _, conn in ipairs(self.lookup:get_connections(source_id)) do local details = conn:details() table.insert(children, { id = details.id, @@ -261,12 +261,12 @@ function Handler:layout() page_size = tonumber(result["page size"]), } -- parse page size to int - pcall(self.add_connection, self, spec --[[@as connection_details]], loader_id) + pcall(self.add_connection, self, spec --[[@as connection_details]], source_id) cb() end, }) end, - -- remove connection (also trigger the loader function) + -- remove connection (also trigger the source's function) action_3 = function(cb) vim.ui.input({ prompt = 'confirm deletion of "' .. details.name .. '"', default = "Y" }, function(input) if not input or string.lower(input) ~= "y" then @@ -284,10 +284,10 @@ function Handler:layout() if #children > 0 then table.insert(layout, { - id = "__loader__" .. loader_id, - name = loader_id, + id = "__source__" .. source_id, + name = source_id, do_expand = true, - type = "loader", + type = "source", children = children, }) end diff --git a/lua/dbee/handler/lookup.lua b/lua/dbee/handler/lookup.lua index 27a0cc98..7efbecbc 100644 --- a/lua/dbee/handler/lookup.lua +++ b/lua/dbee/handler/lookup.lua @@ -1,11 +1,11 @@ local utils = require("dbee.utils") --- Lookup is a "dumb" storage for loaders and connections +-- Lookup is a "dumb" storage for sources and connections -- and their relations ---@class Lookup ---@field private connections table ----@field private loaders table ----@field private conn_lookup table +---@field private sources table +---@field private conn_lookup table ---@field private active_connection conn_id local Lookup = {} @@ -13,7 +13,7 @@ local Lookup = {} function Lookup:new() local o = { connections = {}, - loaders = {}, + sources = {}, conn_lookup = {}, } setmetatable(o, self) @@ -21,28 +21,28 @@ function Lookup:new() return o end ----@param loader Loader -function Lookup:add_loader(loader) - local id = loader:name() +---@param source Source +function Lookup:add_source(source) + local id = source:name() - if self.loaders[id] then - error("loader already exists: " .. id) + if self.sources[id] then + error("source already exists: " .. id) end - self.loaders[id] = { - loader = loader, + self.sources[id] = { + source = source, connections = {}, active_connection = "", } end ----@param id loader_id -function Lookup:remove_loader(id) - if not self.loaders[id] then +---@param id source_id +function Lookup:remove_source(id) + if not self.sources[id] then return end - for _, conn_id in ipairs(self.loaders[id].connections) do + for _, conn_id in ipairs(self.sources[id].connections) do local conn = self.connections[conn_id] if conn then pcall(conn.close, conn) @@ -51,14 +51,14 @@ function Lookup:remove_loader(id) self.conn_lookup[conn_id] = nil end - self.loaders[id] = nil + self.sources[id] = nil end ---@param connection Conn ----@param loader_id loader_id -function Lookup:add_connection(connection, loader_id) - if not loader_id then - error("loader_id not set") +---@param source_id source_id +function Lookup:add_connection(connection, source_id) + if not source_id then + error("source_id not set") end local id = connection:details().id @@ -69,8 +69,8 @@ function Lookup:add_connection(connection, loader_id) end self.connections[id] = connection - self.conn_lookup[id] = loader_id - table.insert(self.loaders[loader_id].connections, id) + self.conn_lookup[id] = source_id + table.insert(self.sources[source_id].connections, id) self.active_connection = id end @@ -86,11 +86,11 @@ function Lookup:remove_connection(id) pcall(conn.close, conn) -- remove the connection from all lookups - local loader_id = self.conn_lookup[id] - if self.loaders[loader_id] and self.loaders[loader_id].connections then - for i, c_id in ipairs(self.loaders[loader_id].connections) do + local source_id = self.conn_lookup[id] + if self.sources[source_id] and self.sources[source_id].connections then + for i, c_id in ipairs(self.sources[source_id].connections) do if id == c_id then - table.remove(self.loaders[loader_id].connections, i) + table.remove(self.sources[source_id].connections, i) end end end @@ -103,16 +103,16 @@ function Lookup:remove_connection(id) end end ----@param loader_id? loader_id # id of the loader or all +---@param source_id? source_id # id of the source or all ---@return Conn[] connections -function Lookup:get_connections(loader_id) +function Lookup:get_connections(source_id) local conns = {} - -- get connections of a loader + -- get connections of a source -- or get all connections - if loader_id then - local l = self.loaders[loader_id] + if source_id then + local l = self.sources[source_id] if not l then - error("unknown loader: " .. loader_id) + error("unknown source: " .. source_id) end for _, c_id in ipairs(l.connections) do table.insert(conns, self.connections[c_id]) @@ -150,39 +150,39 @@ function Lookup:set_active_connection(id) end ---@param conn_id? conn_id id of the connection or all ----@return Loader[] loaders -function Lookup:get_loaders(conn_id) - local loaders = {} - -- get loader of a connection - -- or get all loaders +---@return Source[] sources +function Lookup:get_sources(conn_id) + local sources = {} + -- get source of a connection + -- or get all sources if conn_id then local l_id = self.conn_lookup[conn_id] if not l_id then return {} end - table.insert(loaders, self.loaders[l_id].loader) + table.insert(sources, self.sources[l_id].source) else - for _, l in pairs(self.loaders) do - table.insert(loaders, l.loader) + for _, l in pairs(self.sources) do + table.insert(sources, l.source) end end -- sort keys - table.sort(loaders, function(k1, k2) + table.sort(sources, function(k1, k2) return k1:name() < k2:name() end) - return loaders + return sources end ----@param id loader_id ----@return Loader|nil loader -function Lookup:get_loader(id) - local l = self.loaders[id] +---@param id source_id +---@return Source|nil source +function Lookup:get_source(id) + local l = self.sources[id] if not l then return end - return l.loader + return l.source end return Lookup diff --git a/lua/dbee/loader.lua b/lua/dbee/sources.lua similarity index 77% rename from lua/dbee/loader.lua rename to lua/dbee/sources.lua index 4cd9e432..3f6b4e0f 100644 --- a/lua/dbee/loader.lua +++ b/lua/dbee/sources.lua @@ -2,23 +2,22 @@ local utils = require("dbee.utils") local M = {} ----@alias loader_id string +---@alias source_id string ----@class Loader ----@field name fun(self: Loader):string function to return the name of the loader ----@field load fun(self: Loader):connection_details[] function to load connections from external source ----@field save? fun(self: Loader, conns: connection_details[], action: "add"|"delete") function to save connections to external source (optional) ----@field source? fun(self: Loader):string function which returns a source file to edit (optional) +---@class Source +---@field name fun(self: Source):string function to return the name of the source +---@field load fun(self: Source):connection_details[] function to load connections from external source +---@field save? fun(self: Source, conns: connection_details[], action: "add"|"delete") function to save connections to external source (optional) +---@field file? fun(self: Source):string function which returns a source file to edit (optional) ---- File loader ----@class FileLoader: Loader +---@class FileSource: Source ---@field private path string path to file -M.FileLoader = {} +M.FileSource = {} --- Loads connections from json file ---@param path string path to file ----@return Loader -function M.FileLoader:new(path) +---@return Source +function M.FileSource:new(path) if not path then error("no path provided") end @@ -31,12 +30,12 @@ function M.FileLoader:new(path) end ---@return string -function M.FileLoader:name() +function M.FileSource:name() return vim.fs.basename(self.path) end ---@return connection_details[] -function M.FileLoader:load() +function M.FileSource:load() local path = self.path ---@type connection_details[] @@ -56,7 +55,7 @@ function M.FileLoader:load() local contents = table.concat(lines, "\n") local ok, data = pcall(vim.fn.json_decode, contents) if not ok then - utils.log("warn", 'Could not parse json file: "' .. path .. '".', "loader") + utils.log("warn", 'Could not parse json file: "' .. path .. '".', "sources") return {} end @@ -72,7 +71,7 @@ end -- saves connection to file ---@param conns connection_details[] ---@param action "add"|"delete" -function M.FileLoader:save(conns, action) +function M.FileSource:save(conns, action) local path = self.path if not conns or vim.tbl_isempty(conns) then @@ -114,7 +113,7 @@ function M.FileLoader:save(conns, action) -- write back to file local ok, json = pcall(vim.fn.json_encode, new) if not ok then - utils.log("error", "Could not convert connection list to json", "loader") + utils.log("error", "Could not convert connection list to json", "sources") return end @@ -125,19 +124,18 @@ function M.FileLoader:save(conns, action) end ---@return string -function M.FileLoader:source() +function M.FileSource:source() return self.path end ---- Environment loader ----@class EnvLoader: Loader +---@class EnvSource: Source ---@field private var string path to file -M.EnvLoader = {} +M.EnvSource = {} --- Loads connections from json file ---@param var string env var to load from ----@return Loader -function M.EnvLoader:new(var) +---@return Source +function M.EnvSource:new(var) if not var then error("no path provided") end @@ -150,12 +148,12 @@ function M.EnvLoader:new(var) end ---@return string -function M.EnvLoader:name() +function M.EnvSource:name() return self.var end ---@return connection_details[] -function M.EnvLoader:load() +function M.EnvSource:load() ---@type connection_details[] local conns = {} @@ -166,7 +164,7 @@ function M.EnvLoader:load() local ok, data = pcall(vim.fn.json_decode, raw) if not ok then - utils.log("warn", 'Could not parse connections from env: "' .. self.var .. '".', "loader") + utils.log("warn", 'Could not parse connections from env: "' .. self.var .. '".', "sources") return {} end @@ -179,15 +177,14 @@ function M.EnvLoader:load() return conns end ---- Environment loader ----@class MemoryLoader: Loader +---@class MemorySource: Source ---@field conns connection_details[] -M.MemoryLoader = {} +M.MemorySource = {} --- Loads connections from json file ---@param conns connection_details[] ----@return Loader -function M.MemoryLoader:new(conns) +---@return Source +function M.MemorySource:new(conns) local o = { conns = conns or {}, } @@ -197,12 +194,12 @@ function M.MemoryLoader:new(conns) end ---@return string -function M.MemoryLoader:name() +function M.MemorySource:name() return "memory" end ---@return connection_details[] -function M.MemoryLoader:load() +function M.MemorySource:load() return self.conns end From 021c0fcd4e06e1f5dddc9a5d4d657db054598e6e Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Tue, 23 May 2023 20:20:45 +0200 Subject: [PATCH 21/25] - directly specify sources - fix page size config --- lua/dbee.lua | 29 ++++++----------------------- lua/dbee/config.lua | 26 ++++++++------------------ lua/dbee/handler/conn.lua | 4 ++-- lua/dbee/handler/init.lua | 32 +++++++++++++------------------- lua/dbee/result.lua | 2 +- lua/dbee/sources.lua | 2 +- 6 files changed, 31 insertions(+), 64 deletions(-) diff --git a/lua/dbee.lua b/lua/dbee.lua index 8d170ed4..7f364631 100644 --- a/lua/dbee.lua +++ b/lua/dbee.lua @@ -3,9 +3,6 @@ local Editor = require("dbee.editor") local Result = require("dbee.result") local Ui = require("dbee.ui") local Handler = require("dbee.handler") -local MemorySource = require("dbee.sources").MemorySource -local EnvSource = require("dbee.sources").EnvSource -local FileSource = require("dbee.sources").FileSource local install = require("dbee.install") local utils = require("dbee.utils") local default_config = require("dbee.config").default @@ -54,20 +51,8 @@ local function lazy_setup() }, } - -- handler and sources - -- memory source loads configs from setup() and is also a default source - local mem_source = MemorySource:new(m.config.connections) - - local sources = {} - for _, file in ipairs(m.config.connection_sources.files) do - table.insert(sources, FileSource:new(file)) - end - for _, var in ipairs(m.config.connection_sources.env_vars) do - table.insert(sources, EnvSource:new(var)) - end - -- set up modules - m.handler = Handler:new(result_ui, mem_source, sources) + m.handler = Handler:new(result_ui, m.config.sources, { fallback_page_size = m.config.page_size }) m.result = Result:new(result_ui, m.handler, m.config.result) m.editor = Editor:new(editor_ui, m.handler, m.config.editor) m.drawer = Drawer:new(drawer_ui, m.handler, m.editor, m.config.drawer) @@ -98,10 +83,7 @@ function M.setup(o) local opts = vim.tbl_deep_extend("force", default_config, o) -- validate config vim.validate { - connections = { opts.connections, "table" }, - connection_sources = { opts.connection_sources, "table" }, - connection_sources_files = { opts.connection_sources.files, "table" }, - connection_sources_env_vars = { opts.connection_sources.env_vars, "table" }, + sources = { opts.sources, "table" }, lazy = { opts.lazy, "boolean" }, extra_helpers = { opts.extra_helpers, "table" }, -- submodules @@ -129,12 +111,13 @@ function M.setup(o) pcall_lazy_setup() end ----@param connection connection_details -function M.add_connection(connection) +---@param params connection_details +---@param source_id source_id id of the source to save connection to +function M.add_connection(params, source_id) if not pcall_lazy_setup() then return end - m.handler:add_connection(connection) + m.handler:add_connection(params, source_id) end function M.open() diff --git a/lua/dbee/config.lua b/lua/dbee/config.lua index 5fcb2150..3f658709 100644 --- a/lua/dbee/config.lua +++ b/lua/dbee/config.lua @@ -16,10 +16,10 @@ local m = {} -- configuration object ---@class Config ----@field connections connection_details[] list of configured database connections ----@field connection_sources { files: string[], env_vars: string[] } +---@field sources Source[] list of connection sources ---@field extra_helpers table extra table helpers to provide besides built-ins. example: { postgres = { List = "select..." } ---@field lazy boolean lazy load the plugin or not? +---@field page_size integer ---@field drawer drawer_config ---@field editor editor_config ---@field result result_config @@ -32,21 +32,10 @@ M.default = { -- lazy load the plugin or not? lazy = false, - -- list of manually specified connections - connections = { - -- example: - -- { - -- name = "example-pg", - -- type = "postgres", - -- url = "postgres://user:password@localhost:5432/db?sslmode=disable", - -- }, - }, -- loads connections from files and environment variables - connection_sources = { - -- list of files to load connections from - files = { vim.fn.stdpath("cache") .. "/dbee/persistence.json" }, - -- list of env vars to load connections from - env_vars = { "DBEE_CONNECTIONS" }, + sources = { + require("dbee.sources").EnvSource:new("DBEE_CONNECTIONS"), + require("dbee.sources").FileSource:new(vim.fn.stdpath("cache") .. "/dbee/persistence.json"), }, -- extra table helpers per connection type extra_helpers = { @@ -56,6 +45,9 @@ M.default = { -- }, }, + -- number of rows in the results set to display per page + page_size = 100, + -- drawer window config drawer = { -- mappings for the buffer @@ -145,8 +137,6 @@ M.default = { -- results window config result = { - -- number of rows per page - page_size = 100, -- mappings for the buffer mappings = { -- next/previous page diff --git a/lua/dbee/handler/conn.lua b/lua/dbee/handler/conn.lua index 43be635b..a15c11dc 100644 --- a/lua/dbee/handler/conn.lua +++ b/lua/dbee/handler/conn.lua @@ -26,7 +26,7 @@ local Conn = {} ---@param ui Ui ---@param helpers Helpers ---@param params connection_details ----@param opts? { on_exec: fun() } +---@param opts? { fallback_page_size: integer, on_exec: fun() } ---@return Conn function Conn:new(ui, helpers, params, opts) params = params or {} @@ -55,7 +55,7 @@ function Conn:new(ui, helpers, params, opts) end local type = utils.type_alias(expanded.type) local id = expanded.id or ("__master_connection_id_" .. expanded.name .. expanded.type .. "__") - local page_size = params.page_size or 100 + local page_size = params.page_size or opts.fallback_page_size or 100 -- register in go local ok = vim.fn.Dbee_register_connection(id, expanded.url, type, tostring(page_size)) diff --git a/lua/dbee/handler/init.lua b/lua/dbee/handler/init.lua index 7ba84102..b76efb5c 100644 --- a/lua/dbee/handler/init.lua +++ b/lua/dbee/handler/init.lua @@ -3,46 +3,38 @@ local Conn = require("dbee.handler.conn") local Helpers = require("dbee.handler.helpers") local Lookup = require("dbee.handler.lookup") ----@alias handler_config { default_page_size: integer } +---@alias handler_config { fallback_page_size: integer } -- Handler is an aggregator of connections ---@class Handler ---@field private ui Ui ui for results ---@field private lookup Lookup lookup for sources and connections ---@field private helpers Helpers query helpers ----@field private default_source_id string ---@field private opts handler_config local Handler = {} ---@param ui Ui ui for displaying results ----@param default_source Source ----@param other_sources? Source[] +---@param sources? Source[] ---@param opts? handler_config ---@return Handler -function Handler:new(ui, default_source, other_sources, opts) +function Handler:new(ui, sources, opts) if not ui then error("no results Ui passed to Handler") end - if not default_source then - error("no default Source passed to Handler") - end -- class object local o = { ui = ui, lookup = Lookup:new(), helpers = Helpers:new(), - default_source_id = default_source:name(), opts = opts or {}, } setmetatable(o, self) self.__index = self - -- initialize the default source and others - o:source_add(default_source) - - other_sources = other_sources or {} - for _, source in ipairs(other_sources) do + -- initialize the sources + sources = sources or {} + for _, source in ipairs(sources) do pcall(o.source_add, o, source) end @@ -75,10 +67,10 @@ function Handler:source_reload(id) -- add new connections for _, spec in ipairs(source:load()) do -- create a new connection - spec.page_size = spec.page_size or self.opts.default_page_size ---@type Conn local conn, ok ok, conn = pcall(Conn.new, Conn, self.ui, self.helpers, spec, { + fallback_page_size = self.opts.fallback_page_size, on_exec = function() self:set_active(conn:details().id) end, @@ -94,15 +86,17 @@ end --- adds connection ---@param params connection_details ----@param source_id? source_id id of the source to save connection to +---@param source_id source_id id of the source to save connection to ---@return conn_id # id of the added connection function Handler:add_connection(params, source_id) - source_id = source_id or self.default_source_id + if not source_id then + error("no source id provided") + end -- create a new connection - params.page_size = params.page_size or self.opts.default_page_size ---@type Conn local conn, ok ok, conn = pcall(Conn.new, Conn, self.ui, self.helpers, params, { + fallback_page_size = self.opts.fallback_page_size, on_exec = function() self:set_active(conn:details().id) end, @@ -183,7 +177,7 @@ function Handler:layout() local children = {} -- source can save edits - if type(source.save) == "function" or source_id == self.default_source_id then + if type(source.save) == "function" then table.insert(children, { id = "__source_add_connection__" .. source_id, name = "add", diff --git a/lua/dbee/result.lua b/lua/dbee/result.lua index 70a412b8..3653e2c4 100644 --- a/lua/dbee/result.lua +++ b/lua/dbee/result.lua @@ -1,4 +1,4 @@ ----@alias result_config { mappings: table, page_size: integer } +---@alias result_config { mappings: table } -- Result is a wrapper around the go code -- it is the central part of the plugin and manages connections. diff --git a/lua/dbee/sources.lua b/lua/dbee/sources.lua index 3f6b4e0f..d42cf1a1 100644 --- a/lua/dbee/sources.lua +++ b/lua/dbee/sources.lua @@ -124,7 +124,7 @@ function M.FileSource:save(conns, action) end ---@return string -function M.FileSource:source() +function M.FileSource:file() return self.path end From 94d2d9d4bcb9b3732e680bb44ce8a40a830ce241 Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Tue, 23 May 2023 20:38:36 +0200 Subject: [PATCH 22/25] updated readme for specifying connections --- README.md | 100 ++++++++++++++++++++++++------------------------------ 1 file changed, 45 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index fe14493f..8c9410ac 100644 --- a/README.md +++ b/README.md @@ -195,14 +195,14 @@ Here are a few steps to quickly get started: - All nodes: - - Press `o` to toggle the tree node, `e` to expand and `c` to close. + - Press `o` to toggle the tree node. - Press `r` to manually refresh the tree. - Connections: - - Press `da` to set it as the active one. - - Press `dd` to delete it (this also triggers the `remove` function - see - more below.) + - Press `cw` to edit the connection + - Press `dd` to delete it (if source supports saving, it's also removed from + there - see more below.) - Press `` to perform an action - view history or look at helper queries. @@ -212,7 +212,7 @@ Here are a few steps to quickly get started: - When you try to save it to disk (`:w`), the path is automatically filled for you. You can change the name to anything you want, if you save it to the suggested directory, it will load the next time you open DBee. - - Press `da` to rename the scratchpad. + - Press `cw` to rename the scratchpad. - Press `dd` to delete it (also from disk). - Pressing `` on an existing scratchpad in the drawer will open it in the editor pane. @@ -257,47 +257,43 @@ This is how it looks like: ```lua { - id = "optional_identifier" -- useful to set manually if you want to remove from the file (see below) - -- IT'S YOUR JOB TO KEEP THESE UNIQUE! + id = "optional_identifier" -- only mandatory if you edit a file by hand. IT'S YOUR JOB TO KEEP THESE UNIQUE! name = "My Database", type = "sqlite", -- type of database driver url = "~/path/to/mydb.db", } ``` -There are a few different ways you can use to specify the connection parameters -for DBee: +The connections are loaded to dbee using so-called "sources". They can be added +to dbee using the `setup()` function: -- Using the `setup()` function: - - The most straightforward (but probably the most useless) way is to just add - them to your configuration in `init.lua` like this: - - ```lua +```lua require("dbee").setup { - connections = { - { - name = "My Database", - type = "sqlite", -- type of database driver - url = "~/path/to/mydb.db", + sources = { + require("dbee.sources").MemorySource:new({ + { + name = "...", + type = "...", + url = "...", + }, + -- ... + }), + require("dbee.sources").EnvSource:new("DBEE_CONNECTIONS"), + require("dbee.sources").FileSource:new(vim.fn.stdpath("cache") .. "/dbee/persistence.json"), }, -- ... }, -- ... the rest of your config } - ``` -- Use the prompt at runtime: +``` - You can add connections manually using the "add connection" item in the - drawer. Fill in the values and write the buffer (`:w`) to save the connection. - By default, this will save the connection to the global connections file and - will persist over restarts. +The above sources are just built-ins. Here is a short description of them: -- Use an env variable. This variable is `DBEE_CONNECTIONS` by default: +- `MemorySource` just loads the connections you give it as an argument. - You can export an environment variable with connections from your shell like - this: +- `EnvSource` loads connection from an environment variable Just export the + variable you gave to the loader and you are good to go: ```sh export DBEE_CONNECTIONS='[ @@ -309,35 +305,29 @@ for DBee: ]' ``` -- Use a custom load function: +- `FileSource` loads connections from a given json file. It also supports + editing and adding connections interactively - If you aren't satisfied with the default capabilities, you can provide your - own `load` function in the config at setup. This example uses a - project-specific connections config file: +If the source supports saving and editing you can add connections manually using +the "add" item in the drawer. Fill in the values and write the buffer (`:w`) to +save the connection. By default, this will save the connection to the global +connections file and will persist over restarts (because default `FileSource` +supports saving) - ```lua - local file = vim.fn.getcwd() .. "/.dbee.json" +Another option is to use "edit" item in the tree and just edit the source +manually. - require("dbee").setup { - loader = { - -- this function must return a list of connections and it doesn't - -- care about anything else - load = function() - return require("dbee.loader").load_from_file(file) - end, - -- just as an example you can also specify this function to save any - -- connections from the prompt input to the same file as they are being loaded from - add = function(connections) - require("dbee.loader").add_to_file(file) - end, - -- and this to remove them - remove = function(connections) - require("dbee.loader").remove_from_file(file) - end, - }, - -- ... the rest of your config - } - ``` +If you aren't satisfied with the default capabilities, you can implement your +own source. You just need to fill the following interface and pass it to config +at setup. + +```lua +---@class Source +---@field name fun(self: Source):string function to return the name of the source +---@field load fun(self: Source):connection_details[] function to load connections from external source +---@field save? fun(self: Source, conns: connection_details[], action: "add"|"delete") function to save connections to external source (optional) +---@field file? fun(self: Source):string function which returns a source file to edit (optional) +``` #### Secrets From 16d57a3718f7cf7e81e272525c624524bd84f2fa Mon Sep 17 00:00:00 2001 From: Andrej Kenda Date: Thu, 25 May 2023 06:05:02 +0200 Subject: [PATCH 23/25] added breaking changes warning --- lua/dbee.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lua/dbee.lua b/lua/dbee.lua index 7f364631..e4f35374 100644 --- a/lua/dbee.lua +++ b/lua/dbee.lua @@ -103,6 +103,11 @@ function M.setup(o) ui_post_close_hook = { opts.ui.post_close_hook, "function" }, } + --TODO remove + if opts.connections or opts.editor.window_command or opts.result.window_command or opts.drawer.window_command or opts.result.page_size then + utils.log("warn", "DBee - breaking changes! - see the pinned issue on github for more info") + end + m.config = opts if m.config.lazy then From ebb2f285d0055fc6666c5e9c60cad8299b15dd58 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Thu, 25 May 2023 04:05:43 +0000 Subject: [PATCH 24/25] [docgen] Update doc/dbee.txt --- doc/dbee.txt | 468 ++++++++++++++++++++++++++------------------------- 1 file changed, 242 insertions(+), 226 deletions(-) diff --git a/doc/dbee.txt b/doc/dbee.txt index 060378f3..73401bd2 100644 --- a/doc/dbee.txt +++ b/doc/dbee.txt @@ -8,12 +8,12 @@ CONTENTS *dbee-content 1.1. Installation..........................................|dbee-installation| 1.1.1. Platform Support............................|dbee-platform_support| 1.1.2. Manual Binary Installation........|dbee-manual_binary_installation| - 1.2. Usage........................................................|dbee-usage| - 1.2.1. Specifying Connections................|dbee-specifying_connections| - 1.2.1.1. Secrets........................................|dbee-secrets| - 1.3. Configuration........................................|dbee-configuration| + 1.2. Configuration........................................|dbee-configuration| + 1.3. Usage........................................................|dbee-usage| + 1.3.1. Getting Started..............................|dbee-getting_started| + 1.3.2. Specifying Connections................|dbee-specifying_connections| + 1.3.2.1. Secrets........................................|dbee-secrets| 1.4. Projector Integration........................|dbee-projector_integration| - 1.5. Development............................................|dbee-development| 2. DBee Architecture Overview....................|dbee-dbee_architecture_overview| 2.1. Lua Architecture..................................|dbee-lua_architecture| 2.2. Go Architecture....................................|dbee-go_architecture| @@ -119,194 +119,20 @@ users. If that doesn't include you, then you have a few options: go build [-o ~/.local/share/nvim/dbee/bin/dbee] < --------------------------------------------------------------------------------- -USAGE *dbee-usage* - -Call the `setup()` function with an optional config parameter. If you are not -using your plugin manager to lazy load for you, make sure to specify -`{ lazy = true }` in the config. - -Here is a brief refference of the most useful functions: -> - -- Open/close the UI. - require("dbee").open() - require("dbee").close() - -- Next/previou page of the results (there are the same mappings that work just inside the results buffer - -- available in config). - require("dbee").next() - require("dbee").prev() - -- Run a query on the active connection directly. - require("dbee").execute(query) - -- Save the current result to file (format is either "csv" or "json" for now). - require("dbee").save(format, file) -< - -SPECIFYING CONNECTIONS *dbee-specifying_connections* - -Connection represents an instance of the database client (i.e. one database). -This is how it looks like: -> - { - id = "optional_identifier" -- useful to set manually if you want to remove from the file (see below) - -- IT'S YOUR JOB TO KEEP THESE UNIQUE! - name = "My Database", - type = "sqlite", -- type of database driver - url = "~/path/to/mydb.db", - } -< - -There are a few different ways you can use to specify the connection parameters -for DBee: - -* Using the `setup()` function: - -The most straightforward (but probably the most useless) way is to just add - them to your configuration in `init.lua` like this: -> - require("dbee").setup { - connections = { - { - name = "My Database", - type = "sqlite", -- type of database driver - url = "~/path/to/mydb.db", - }, - -- ... - }, - -- ... the rest of your config - } -< - -* Use the prompt at runtime: - -You can add connections manually using the "add connection" item in the - drawer. Fill in the values and write the buffer (`:w`) to save the connection. - By default, this will save the connection to the global connections file and - will persist over restarts. - -* Use an env variable. This variable is `DBEE_CONNECTIONS` by default: - -You can export an environment variable with connections from your shell like - this: -> - export DBEE_CONNECTIONS='[ - { - "name": "DB from env", - "url": "mysql://...", - "type": "mysql" - } - ]' -< - -* Use a custom load function: - -If you aren't satisfied with the default capabilities, you can provide your - own `load` function in the config at setup. This example uses a - project-specific connections config file: -> - local file = vim.fn.getcwd() .. "/.dbee.json" - require("dbee").setup { - loader = { - -- this function must return a list of connections and it doesn't - -- care about anything else - load = function() - return require("dbee.loader").load_from_file(file) - end, - -- just as an example you can also specify this function to save any - -- connections from the prompt input to the same file as they are being loaded from - add = function(connections) - require("dbee.loader").add_to_file(file) - end, - -- and this to remove them - remove = function(connections) - require("dbee.loader").remove_from_file(file) - end, - }, - -- ... the rest of your config - } -< - -SECRETS *dbee-secrets* - -If you don't want to have secrets laying around your disk in plain text, you can -use the special placeholders in connection strings (this works using any method -for specifying connections). - -NOTE: Currently only envirnoment variables are supported - -Example: - -Using the `DBEE_CONNECTIONS` environment variable for specifying connections and -exporting secrets to environment: -> - # Define connections - export DBEE_CONNECTIONS='[ - { - "name": "{{ env.SECRET_DB_NAME }}", - "url": "postgres://{{ env.SECRET_DB_USER }}:{{ env.SECRET_DB_PASS }}@localhost:5432/{{ env.SECRET_DB_NAME }}?sslmode=disable", - "type": "postgres" - } - ]' - # Export secrets - export SECRET_DB_NAME="secretdb" - export SECRET_DB_USER="secretuser" - export SECRET_DB_PASS="secretpass" -< - -If you start neovim in the same shell, this will evaluate to the following -connection: -> - { { - name = "secretdb", - url = "postgres://secretuser:secretpass@localhost:5432/secretdb?sslmode=disable", - type = "postgres", - } } -< - -------------------------------------------------------------------------------- CONFIGURATION *dbee-configuration* -As mentioned, you can pass an optional table parameter to `setup()` function. +You can pass an optional table parameter to `setup()` function. Here are the defaults: > M.default = { -- lazy load the plugin or not? lazy = false, - -- list of manually specified connections - connections = { - -- example: - -- { - -- name = "example-pg", - -- type = "postgres", - -- url = "postgres://user:password@localhost:5432/db?sslmode=disable", - -- }, - }, - -- load/save functionality for connectoins - -- you can use helper function from require("dbee.loader") - -- or come up with something completly different - loader = { - -- you can control what happens with this function, - -- when the application wants to save connections - for example from "Add Connection" prompt - -- recieves a list of connections - add = function(connections) - -- append to default file - require("dbee.loader").add_to_file(connections) - end, - -- you can control what happens with this function, - -- when the application wants to remove connections - for example from drawer action - -- recieves a list of connections - remove = function(connections) - -- remove from default file - require("dbee.loader").remove_from_file(connections) - end, - -- use this function to provide different connections from files, env... - -- must return a list of connections - load = function() - -- load from default env var and file - local file_conns = require("dbee.loader").load_from_file() - local env_conns = require("dbee.loader").load_from_env() - return vim.list_extend(file_conns, env_conns) - end, + -- loads connections from files and environment variables + sources = { + require("dbee.sources").EnvSource:new("DBEE_CONNECTIONS"), + require("dbee.sources").FileSource:new(vim.fn.stdpath("cache") .. "/dbee/persistence.json"), }, -- extra table helpers per connection type extra_helpers = { @@ -315,11 +141,10 @@ Here are the defaults: -- ["List All"] = "select * from {table}", -- }, }, + -- number of rows in the results set to display per page + page_size = 100, -- drawer window config drawer = { - -- command that opens the window if the window is closed - -- string or function - window_command = "to 40vsplit", -- mappings for the buffer mappings = { -- manually refresh drawer @@ -328,45 +153,58 @@ Here are the defaults: -- action_1 opens a scratchpad or executes a helper action_1 = { key = "", mode = "n" }, -- action_2 renames a scratchpad or sets the connection as active manually - action_2 = { key = "da", mode = "n" }, + action_2 = { key = "cw", mode = "n" }, -- action_3 deletes a scratchpad or connection (removes connection from the file if you configured it like so) action_3 = { key = "dd", mode = "n" }, -- these are self-explanatory: - collapse = { key = "c", mode = "n" }, - expand = { key = "e", mode = "n" }, + -- collapse = { key = "c", mode = "n" }, + -- expand = { key = "e", mode = "n" }, toggle = { key = "o", mode = "n" }, }, -- icon settings: - disable_icons = false, - icons = { + disable_candies = false, + candies = { -- these are what's available for now: history = { icon = "", - highlight = "Constant", + icon_highlight = "Constant", }, scratch = { icon = "", - highlight = "Character", + icon_highlight = "Character", }, database = { icon = "", - highlight = "SpecialChar", + icon_highlight = "SpecialChar", }, table = { icon = "", - highlight = "Conditional", + icon_highlight = "Conditional", }, add = { icon = "", - highlight = "String", + icon_highlight = "String", + text_highlight = "String", + }, + edit = { + icon = "󰏫", + icon_highlight = "Directory", + text_highlight = "Directory", }, remove = { icon = "󰆴", - highlight = "SpellBad", + icon_highlight = "SpellBad", + text_highlight = "SpellBad", }, help = { icon = "󰋖", - highlight = "NormalFloat", + icon_highlight = "Title", + text_highlight = "Title", + }, + source = { + icon = "󰃖", + icon_highlight = "MoreMsg", + text_highlight = "MoreMsg", }, -- if there is no type -- use this for normal nodes... @@ -376,26 +214,21 @@ Here are the defaults: -- ...and use this for nodes with children none_dir = { icon = "", - highlight = "NonText", + icon_highlight = "NonText", }, -- chevron icons for expanded/closed nodes node_expanded = { icon = "", - highlight = "NonText", + icon_highlight = "NonText", }, node_closed = { icon = "", - highlight = "NonText", + icon_highlight = "NonText", }, }, }, -- results window config result = { - -- command that opens the window if the window is closed - -- string or function - window_command = "bo 15split", - -- number of rows per page - page_size = 100, -- mappings for the buffer mappings = { -- next/previous page @@ -405,14 +238,6 @@ Here are the defaults: }, -- editor window config editor = { - -- command that opens the window if the window is closed - -- string or function - window_command = function() - vim.cmd("new") - vim.cmd("only") - m.tmp_buf = vim.api.nvim_get_current_buf() - return vim.api.nvim_get_current_win() - end, -- mappings for the buffer mappings = { -- run what's currently selected on the active connection @@ -429,6 +254,18 @@ Here are the defaults: -- -- You can probably do anything you imagine with this - for example all floating windows, tiled/floating mix etc. ui = { + -- commands that opens the window if the window is closed - for drawer/editor/result + -- string or function + window_commands = { + drawer = "to 40vsplit", + result = "bo 15split", + editor = function() + vim.cmd("new") + vim.cmd("only") + m.tmp_buf = vim.api.nvim_get_current_buf() + return vim.api.nvim_get_current_win() + end, + }, -- how to open windows in order (with specified "window_command"s -- see above) window_open_order = { "editor", "result", "drawer" }, -- hooks before/after dbee.open()/.close() @@ -449,6 +286,189 @@ Here are the defaults: } < +-------------------------------------------------------------------------------- +USAGE *dbee-usage* + +Call the `setup()` function with an optional config parameter. If you are not +using your plugin manager to lazy load for you, make sure to specify +`{ lazy = true }` in the config. +> + -- Open/close the UI. + require("dbee").open() + require("dbee").close() + -- Next/previou page of the results (there are the same mappings that work just inside the results buffer + -- available in config). + require("dbee").next() + require("dbee").prev() + -- Run a query on the active connection directly. + require("dbee").execute(query) + -- Save the current result to file (format is either "csv" or "json" for now). + require("dbee").save(format, file) +< + +GETTING STARTED *dbee-getting_started* + +Here are a few steps to quickly get started: + +* call the `setup()` function in your `init.lua` +* Specify connections using one or more sources (reffer to + this section (#specifying-connections)). +* When you restart the editor, call `lua require("dbee").open()` to open the UI. +* Navigate to the drawer (tree) and use the following key-bindings to perform + different actions depending on the context (the mappings can all be changed in + the config): + * All nodes: + * Press `o` to toggle the tree node. + * Press `r` to manually refresh the tree. + * Connections: + * Press `cw` to edit the connection + * Press `dd` to delete it (if source supports saving, it's also removed from + there - see more below.) + * Press `` to perform an action - view history or look at helper + queries. + * Scratchpads: + * Press `` on the `new` node to create a new scratchpad. + * When you try to save it to disk (`:w`), the path is automatically filled + for you. You can change the name to anything you want, if you save it to + the suggested directory, it will load the next time you open DBee. + * Press `cw` to rename the scratchpad. + * Press `dd` to delete it (also from disk). + * Pressing `` on an existing scratchpad in the drawer will open it in + the editor pane. + * Help: + * Just view the key bindings. +* Once you selected the connection and created a scratchpad, you can navigate to + the editor pane (top-right by default) and start writing queries. In editor + pane, you can use the following actions: + * Highlight some text in visual mode and press `BB` - this will run the + selected query on the active connection. + * If you press `BB` in normal mode, you run the whole scratchpad on the active + connection. +* If the request was successful, the results should appear in the "result" + buffer (bottom one by default). If the total number of results was lower than + the `page_size` parameter in config (100 by default), all results should + already be present. If there are more than `page_size` results, you can "page" + thrugh them using one of the following: + * Using `require("dbee").next()` and `require("dbee").prev()` from anywhere + (even if your cursor is outside the result buffer). + * Using `L` for next and `H` for previous page if the cursor is located inside + the results buffer. +* The current result (of the active connection) can also be saved to a file + using `require("dbee").save()` command. Use: + * `require("dbee").save("csv", "/path/to/file.csv")` for csv and + * `require("dbee").save("json", "/path/to/file.json")` for json. +* Once you are done or you want to go back to where you were, you can call + `require("dbee").close()`. + +SPECIFYING CONNECTIONS *dbee-specifying_connections* + +Connection represents an instance of the database client (i.e. one database). +This is how it looks like: +> + { + id = "optional_identifier" -- only mandatory if you edit a file by hand. IT'S YOUR JOB TO KEEP THESE UNIQUE! + name = "My Database", + type = "sqlite", -- type of database driver + url = "~/path/to/mydb.db", + } +< + +The connections are loaded to dbee using so-called "sources". They can be added +to dbee using the `setup()` function: +> + require("dbee").setup { + sources = { + require("dbee.sources").MemorySource:new({ + { + name = "...", + type = "...", + url = "...", + }, + -- ... + }), + require("dbee.sources").EnvSource:new("DBEE_CONNECTIONS"), + require("dbee.sources").FileSource:new(vim.fn.stdpath("cache") .. "/dbee/persistence.json"), + }, + -- ... + }, + -- ... the rest of your config + } +< + +The above sources are just built-ins. Here is a short description of them: + +* `MemorySource` just loads the connections you give it as an argument. +* `EnvSource` loads connection from an environment variable Just export the + variable you gave to the loader and you are good to go: +> + export DBEE_CONNECTIONS='[ + { + "name": "DB from env", + "url": "mysql://...", + "type": "mysql" + } + ]' +< +* `FileSource` loads connections from a given json file. It also supports + editing and adding connections interactively + +If the source supports saving and editing you can add connections manually using +the "add" item in the drawer. Fill in the values and write the buffer (`:w`) to +save the connection. By default, this will save the connection to the global +connections file and will persist over restarts (because default `FileSource` +supports saving) + +Another option is to use "edit" item in the tree and just edit the source +manually. + +If you aren't satisfied with the default capabilities, you can implement your +own source. You just need to fill the following interface and pass it to config +at setup. +> + ---@class Source + ---@field name fun(self: Source):string function to return the name of the source + ---@field load fun(self: Source):connection_details[] function to load connections from external source + ---@field save? fun(self: Source, conns: connection_details[], action: "add"|"delete") function to save connections to external source (optional) + ---@field file? fun(self: Source):string function which returns a source file to edit (optional) +< + +SECRETS *dbee-secrets* + +If you don't want to have secrets laying around your disk in plain text, you can +use the special placeholders in connection strings (this works using any method +for specifying connections). + +NOTE: Currently only envirnoment variables are supported + +Example: + +Using the `DBEE_CONNECTIONS` environment variable for specifying connections and +exporting secrets to environment: +> + # Define connections + export DBEE_CONNECTIONS='[ + { + "name": "{{ env.SECRET_DB_NAME }}", + "url": "postgres://{{ env.SECRET_DB_USER }}:{{ env.SECRET_DB_PASS }}@localhost:5432/{{ env.SECRET_DB_NAME }}?sslmode=disable", + "type": "postgres" + } + ]' + # Export secrets + export SECRET_DB_NAME="secretdb" + export SECRET_DB_USER="secretuser" + export SECRET_DB_PASS="secretpass" +< + +If you start neovim in the same shell, this will evaluate to the following +connection: +> + { { + name = "secretdb", + url = "postgres://secretuser:secretpass@localhost:5432/secretdb?sslmode=disable", + type = "postgres", + } } +< + -------------------------------------------------------------------------------- PROJECTOR INTEGRATION *dbee-projector_integration* @@ -458,12 +478,6 @@ code-runner/project-configurator. To use dbee with it, simply use `"dbee"` as one of it's outputs. --------------------------------------------------------------------------------- -DEVELOPMENT *dbee-development* - -Reffer to ARCHITECTURE.md (ARCHITECTURE.md) for a brief overview of the -architecture. - ================================================================================ DBEE ARCHITECTURE OVERVIEW *dbee-dbee_architecture_overview* @@ -495,11 +509,11 @@ LUA ARCHITECTURE *dbee-lua_architectur │ │ │ - │ ┌─────────────┐ - │ │ │ - │ │ install │ - │ │ │ - │ └─────────────┘ + │ ┌─────────────┐ ┌────────────┐ + │ │ │ │ │ + │ │ install │ │ loader │ + │ │ │ │ │ + │ └─────────────┘ └────────────┘ │ < @@ -510,6 +524,8 @@ Description: * `install` package is independent of the other packages and is used for installation of the compiled go binary using the manifest generated by the CI pipeline. +* `loader` package is also independent and is used as the default loading and + saving method in the config, which is later consumed by the handler * `drawer` is the "tree" view in UI - it consumes the editor (to provide scratchpad view and to manage scratchpads) and the handler (for managing connections and providing layout of each database). From f45cddf1e01fdc7f3d378d72677739be2cb80e0c Mon Sep 17 00:00:00 2001 From: Github Actions Date: Thu, 25 May 2023 04:14:39 +0000 Subject: [PATCH 25/25] [install] Update lua/dbee/install/__manifest.lua --- lua/dbee/install/__manifest.lua | 42 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lua/dbee/install/__manifest.lua b/lua/dbee/install/__manifest.lua index cefcc5f1..263b9e06 100644 --- a/lua/dbee/install/__manifest.lua +++ b/lua/dbee/install/__manifest.lua @@ -4,29 +4,29 @@ local M = {} -- Links to binary releases M.urls = { - dbee_android_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_android_amd64", - dbee_android_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_android_arm64", - dbee_darwin_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_darwin_amd64", - dbee_darwin_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_darwin_arm64", - dbee_freebsd_386 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_freebsd_386", - dbee_freebsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_freebsd_amd64", - dbee_freebsd_arm = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_freebsd_arm", - dbee_freebsd_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_freebsd_arm64", - dbee_linux_386 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_linux_386", - dbee_linux_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_linux_amd64", - dbee_linux_arm = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_linux_arm", - dbee_linux_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_linux_arm64", - dbee_linux_ppc64le = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_linux_ppc64le", - dbee_linux_riscv64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_linux_riscv64", - dbee_linux_s390x = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_linux_s390x", - dbee_netbsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_netbsd_amd64", - dbee_openbsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_openbsd_amd64", - dbee_openbsd_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_openbsd_arm64", - dbee_windows_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_windows_amd64", - dbee_windows_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/d52e20edc1f05d731cc0d1800145fa368019ecfc/artifacts/dbee_windows_arm64", + dbee_android_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_android_amd64", + dbee_android_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_android_arm64", + dbee_darwin_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_darwin_amd64", + dbee_darwin_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_darwin_arm64", + dbee_freebsd_386 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_freebsd_386", + dbee_freebsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_freebsd_amd64", + dbee_freebsd_arm = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_freebsd_arm", + dbee_freebsd_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_freebsd_arm64", + dbee_linux_386 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_linux_386", + dbee_linux_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_linux_amd64", + dbee_linux_arm = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_linux_arm", + dbee_linux_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_linux_arm64", + dbee_linux_ppc64le = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_linux_ppc64le", + dbee_linux_riscv64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_linux_riscv64", + dbee_linux_s390x = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_linux_s390x", + dbee_netbsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_netbsd_amd64", + dbee_openbsd_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_openbsd_amd64", + dbee_openbsd_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_openbsd_arm64", + dbee_windows_amd64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_windows_amd64", + dbee_windows_arm64 = "https://github.com/kndndrj/nvim-dbee-bucket/raw/31b3a28bd9eae7a834a692525e77247272b0ddef/artifacts/dbee_windows_arm64", } -- Current version of go main package -M.version = "fa4cda76d6f356870018ebfe0f3bd64b62dd9282" +M.version = "16d57a3718f7cf7e81e272525c624524bd84f2fa" return M