diff --git a/README.md b/README.md index 5d0dd81..3231e51 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Using [vim-plug](https://github.com/junegunn/vim-plug) ```viml Plug "mickael-menu/zk-nvim" Plug "neovim/nvim-lspconfig" + Plug 'nvim-telescope/telescope.nvim' " optional Plug 'nvim-lua/plenary.nvim' " optional, dependency for Telescope ``` @@ -32,19 +33,13 @@ require("telescope").load_extension("zk") ``` > :warning: This plugin will setup and start the LSP server for you, do *not* call `require("lspconfig").zk.setup()`. -#### Default configuration +**Default configuration** ```lua require("zk").setup({ -- create user commands such as :ZkNew create_user_commands = true, lsp = { - -- automatically attach buffers in a zk notebook that match the given filetypes - auto_attach = { - enabled = true, - filetypes = { "markdown" }, - }, - -- `config` is passed to `vim.lsp.start_client(config)` config = { cmd = { "zk", "lsp" }, @@ -53,24 +48,98 @@ require("zk").setup({ -- on_attach = ... -- etc, see `:h vim.lsp.start_client()` }, + + -- automatically attach buffers in a zk notebook that match the given filetypes + auto_attach = { + enabled = true, + filetypes = { "markdown" }, + }, }, }) ``` +### Notebook Directory Discovery +When you run a notebook command, this plugin will look for a notebook in the following places and order: +1. the current buffer path (i.e. the file you are currently editing), +2. the current working directory, +3. the `$ZK_NOTEBOOK_DIR` environment variable. + +We recommend you to export the `$ZK_NOTEBOOK_DIR` environment variable, so that a notebook can always be found. + +It is worth noting that for some notebook commands you can explicitly specify a notebook by providing a path to any file or directory within the notebook. +An explicitly provided path will always take precedence and override the automatic notebook discovery. +However, this is always optional, and usually not necessary. + ## Commands +### VimL ```vim +" Indexes the notebook :ZkIndex + +" Creates and opens a new note +" params +" (optional) directory for the new note, relative to the notebook root :ZkNew [] + +" Opens a Telescope picker +" params +" (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist +:ZkList [] + +" Opens a Telescope picker +" params +" (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zktaglist +:ZkTagList [] ``` -or via Lua +where `options` can be any valid *Lua* expression that evaluates to a table. + +*Examples:* +```vim +:ZkNew daily +:ZkList { createdAfter = "3 days ago", tags = { "work" } } +``` + +### Lua ```lua -require("zk").index(path, args) -- path and args are optional -require("zk").new(path, args) -- path and args are optional +---Indexes the notebook +-- +---@param path? string path to explicitly specify the notebook +---@param options table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zkindex +require("zk").index(path, options) + +---Creates and opens a new note +-- +---@param path? string path to explicitly specify the notebook +---@param options table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zknew +require("zk").new(path, options) + +---Opens a Telescope picker +-- +---@param path? string path to explicitly specify the notebook +---@param options table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist +require("zk").list(path, options) + +---Opens a Telescope picker +-- +---@param path? string path to explicitly specify the notebook +---@param options table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zktaglist +require("zk").tag.list(path, options) ``` -### Telescope +*Examples:* +```lua +require("zk").new(nil, { dir = "daily" }) +require("zk").list(nil, { createdAfter = "3 days ago", tags = { "work" } }) +``` + +As you can see, the `path` is optional, and can usually be omitted; see [Notebook Directory Discovery](#notebook-directory-discovery). +### Telescope ```vim :Telescope zk notes :Telescope zk orphans @@ -88,27 +157,42 @@ require('telescope').extensions.zk.links() require('telescope').extensions.zk.related() require('telescope').extensions.zk.tags() ``` -By default, this plugin will use the path of the current buffer to determine the location of your notebook. -Note that if the current buffer does not belong to a notebook, `$ZK_NOTEBOOK_DIR` will be used to locate your notebook. -If you want, you can also explicitly specify a notebook by providing the path to any file or folder within the notebook like so `:Telescope zk notes path=/foo/bar` or so `require('telescope').extensions.zk.notes({ path = '/foo/bar'})`. +The Telescope pickers also allow you to explicitly specify a notebook like so `:Telescope zk notes path=/foo/bar` or so `require('telescope').extensions.zk.notes({ path = '/foo/bar'})`. +However, specifing a `path` is optional, and is usually not necessary; see [Notebook Directory Discovery](#notebook-directory-discovery). + +You can even pass the same additional options to the Telescope pickers as described in [list and tag list commands](#commands). + +*Example VimL:* +```vim +:Telescope zk notes createdAfter=3\ days\ ago +``` + +*Example Lua:* +```lua +require('telescope').extensions.zk.notes({ createdAfter = "3 days ago", tags = { "work" } }) +``` + +As you can see, the VimL API is a bit constrained. Whitespace must be escaped and lists and dictionaries are not supported. +It is therefore recommended to use the `:ZkList` and `:ZkTagList` [commands](#commands) instead. ## API -The difference between e.g. `require("zk").api.new` and `require("zk").new` is that the former lets you handle the API results yourself for more flexibility. +The functions in the API module give you maximum flexibility and provide only a thin Lua friendly layer around zk's API. +You can use it to write your own specialized functions for interacting with zk. ```lua -- https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zkindex --- path and args are optional -require("zk").api.index(path, args, function(stats) +-- path and options are optional +require("zk").api.index(path, options, function(stats) -- do something with the stats end) ``` ```lua -- https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zknew --- path and args are optional -require("zk").api.new(path, args, function(res) +-- path and options are optional +require("zk").api.new(path, options, function(res) file_path = res.path -- do something with the new file path end) @@ -116,17 +200,17 @@ end) ```lua -- https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist --- path is optional, args.select is required --- args = { select = { "title", "absPath", "rawContent" }, sort = { "created" } } -require("zk").api.list(path, args, function(notes) +-- path is optional, options.select is required +-- options = { select = { "title", "absPath", "rawContent" }, sort = { "created" } } +require("zk").api.list(path, options, function(notes) -- do something with the notes end) ``` ```lua -- https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zktaglist --- path and args are optional -require("zk").api.tag.list(path, args, function(tags) +-- path and options are optional +require("zk").api.tag.list(path, options, function(tags) -- do something with the tags end) ``` diff --git a/lua/telescope/_extensions/zk.lua b/lua/telescope/_extensions/zk.lua index 7ab2332..daac99b 100644 --- a/lua/telescope/_extensions/zk.lua +++ b/lua/telescope/_extensions/zk.lua @@ -1,46 +1,54 @@ local util = require("telescope.zk.util") local zk = require("zk") +---@param opts table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist local function show_notes(opts) opts = vim.tbl_extend("keep", opts or {}, { prompt_title = "Zk Notes" }) - zk.api.list(opts.path, util.wrap_note_args({}), function(notes) + zk.api.list(opts.path, util.wrap_note_options(opts), function(notes) util.show_note_picker(opts, notes) end) end +---@param opts table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist local function show_orphans(opts) opts = vim.tbl_extend("keep", opts or {}, { prompt_title = "Zk Orphans" }) - zk.api.list(opts.path, util.wrap_note_args({ orphan = true }), function(notes) - util.show_note_picker(opts, notes) - end) + opts = vim.tbl_deep_extend("force", opts, { orphan = true }) + show_notes(opts) end +---@param opts table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist local function show_backlinks(opts) opts = vim.tbl_extend("keep", opts or {}, { prompt_title = "Zk Backlinks" }) - zk.api.list(opts.path, util.wrap_note_args({ linkTo = { vim.api.nvim_buf_get_name(0) } }), function(notes) - util.show_note_picker(opts, notes) - end) + opts = vim.tbl_deep_extend("force", opts, { linkTo = { vim.api.nvim_buf_get_name(0) } }) + show_notes(opts) end +---@param opts table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist local function show_links(opts) opts = vim.tbl_extend("keep", opts or {}, { prompt_title = "Zk Links" }) - zk.api.list(opts.path, util.wrap_note_args({ linkedBy = { vim.api.nvim_buf_get_name(0) } }), function(notes) - util.show_note_picker(opts, notes) - end) + opts = vim.tbl_deep_extend("force", opts, { linkedBy = { vim.api.nvim_buf_get_name(0) } }) + show_notes(opts) end +---@param opts table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist local function show_related(opts) opts = vim.tbl_extend("keep", opts or {}, { prompt_title = "Zk Related" }) - zk.api.list(opts.path, util.wrap_note_args({ related = { vim.api.nvim_buf_get_name(0) } }), function(notes) - util.show_note_picker(opts, notes) - end) + opts = vim.tbl_deep_extend("force", opts, { related = { vim.api.nvim_buf_get_name(0) } }) + show_notes(opts) end +---@param opts table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zktaglist local function show_tags(opts) opts = vim.tbl_extend("keep", opts or {}, { prompt_title = "Zk Tags" }) - zk.api.tag.list(opts.path, util.wrap_tag_args({}), function(tags) + zk.api.tag.list(opts.path, util.wrap_tag_options({}), function(tags) util.show_tag_picker(opts, tags, function(selected_tags) - zk.api.list(opts.path, util.wrap_note_args({ tags = selected_tags }), function(notes) + zk.api.list(opts.path, util.wrap_note_options({ tags = selected_tags }), function(notes) opts.prompt_title = "Zk Notes for tag(s) " .. vim.inspect(selected_tags) util.show_note_picker(opts, notes) end) diff --git a/lua/telescope/zk/util.lua b/lua/telescope/zk/util.lua index 6cef6b8..2176d3c 100644 --- a/lua/telescope/zk/util.lua +++ b/lua/telescope/zk/util.lua @@ -10,7 +10,7 @@ local previewers = require("telescope.previewers") local M = {} -function M.wrap_note_args(opts) +function M.wrap_note_options(opts) return vim.tbl_deep_extend( "force", { select = { "title", "absPath", "rawContent" }, sort = { "created" } }, @@ -18,7 +18,7 @@ function M.wrap_note_args(opts) ) end -function M.wrap_tag_args(opts) +function M.wrap_tag_options(opts) return vim.tbl_deep_extend("force", { sort = { "note-count" } }, opts or {}) end @@ -66,7 +66,6 @@ function M.make_note_previewer() end function M.show_note_picker(opts, notes) - -- zk.api.list(opts.path, zk_args, function(notes) opts = opts or {} pickers.new(opts, { finder = finders.new_table({ @@ -76,11 +75,9 @@ function M.show_note_picker(opts, notes) sorter = conf.file_sorter(opts), previewer = M.make_note_previewer(), }):find() - -- end) end function M.show_tag_picker(opts, tags, cb) - -- zk.api.tag.list(opts.path, make_tag_args(), function(tags) opts = opts or {} pickers.new(opts, { finder = finders.new_table({ @@ -101,13 +98,11 @@ function M.show_tag_picker(opts, tags, cb) actions.close(prompt_bufnr) - -- _list_notes(opts, { prompt_title = "Zk Notes for tag(s) " .. vim.inspect(selection) }, { tags = selection }) cb(selection) end) return true end, }):find() - -- end) end return M diff --git a/lua/zk.lua b/lua/zk.lua index 8117440..ae5a70c 100644 --- a/lua/zk.lua +++ b/lua/zk.lua @@ -7,6 +7,8 @@ M.api = require("zk.api") M.lsp = require("zk.lsp") +---The entry point of the plugin +---@param options? table user configuration options function M.setup(options) config.options = vim.tbl_deep_extend("force", config.defaults, options or {}) @@ -16,22 +18,71 @@ function M.setup(options) if config.options.create_user_commands then vim.cmd("command! ZkIndex lua require('zk').index()") - vim.cmd("command! -nargs=? ZkNew lua require('zk').new(nil, { dir = [=[]=]})") + vim.cmd("command! -nargs=? ZkNew lua require('zk').new(nil, { dir = })") -- the command arg (directory) is interpreted relative to the notebook root + -- vim.cmd("command! -nargs=? -complete=dir ZkNew lua require('zk').new(nil, { dir = vim.fn.fnamemodify(, ':p') })") -- this would interpret the command arg (dir) relative to the cwd instead + vim.cmd( + "command! -nargs=? -complete=lua ZkList lua require('zk').list(nil, assert(loadstring('return ' .. ))())" + ) + vim.cmd( + "command! -nargs=? -complete=lua ZkTagList lua require('zk').tag.list(nil, assert(loadstring('return ' .. ))())" + ) end end -- Commands -function M.index(path, args) - M.api.index(path, args, function(stats) +---Indexes the notebook +-- +---@param path? string path to explicitly specify the notebook +---@param options table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zkindex +function M.index(path, options) + M.api.index(path, options, function(stats) vim.notify(vim.inspect(stats)) end) end -function M.new(path, args) - M.api.new(path, args, function(res) +---Creates and opens a new note +-- +---@param path? string path to explicitly specify the notebook +---@param options table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zknew +function M.new(path, options) + M.api.new(path, options, function(res) vim.cmd("edit " .. res.path) end) end +---Opens a Telescope picker +-- +---@param path? string path to explicitly specify the notebook +---@param options table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist +function M.list(path, options) + -- NOTE: this does not have to be telescope specific. + -- In the future consider exposing something like config.options.picker = 'telescope'|'fzf'|'builtin'. + -- Obviously the same applies to the `M.tag.list` function. + if path then + options = options or {} + options.path = path + end + -- `h: telescope.command` + require("telescope._extensions.zk").exports.notes(options) +end + +M.tag = {} + +---Opens a Telescope picker +-- +---@param path? string path to explicitly specify the notebook +---@param options table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zktaglist +function M.tag.list(path, options) + if path then + options = options or {} + options.path = path + end + require("telescope._extensions.zk").exports.tags(options) +end + return M diff --git a/lua/zk/api.lua b/lua/zk/api.lua index 90cd71f..c5fba5e 100644 --- a/lua/zk/api.lua +++ b/lua/zk/api.lua @@ -3,27 +3,48 @@ local util = require("zk.util") local M = {} +---Try to resolve the notebook directory by checking the following locations in that order +---1. current buffer path +---2. current working directory +---3. `$ZK_NOTEBOOK_DIR` environment variable +-- +---@param bufnr number? local function resolve_notebook_dir(bufnr) local path = vim.api.nvim_buf_get_name(bufnr) - -- if the buffer has no name, use the current working directory + local cwd = vim.fn.getcwd(0) + -- if the buffer has no name (i.e. it is empty), set the current working directory as it's path if path == "" then - path = vim.fn.getcwd(0) + path = cwd end - -- if the buffer doesn't belong to a notebook, use $ZK_NOTEBOOK_DIR as fallback if available - if not util.is_notebook_path(path) and vim.env.ZK_NOTEBOOK_DIR then - path = vim.env.ZK_NOTEBOOK_DIR + if not util.notebook_root(path) then + if not util.notebook_root(cwd) then + -- if neither the buffer nor the cwd belong to a notebook, use $ZK_NOTEBOOK_DIR as fallback if available + if vim.env.ZK_NOTEBOOK_DIR then + path = vim.env.ZK_NOTEBOOK_DIR + end + else + -- the buffer doesn't belong to a notebook, but the cwd does! + path = cwd + end end + -- at this point, the buffer either belongs to a notebook, or everything else failed return path end -local function execute_command(path, cmd, args, cb) +---Executes the given command via LSP +-- +---@param cmd string +---@param path string? +---@param options table? +---@param cb function? +local function execute_command(cmd, path, options, cb) local bufnr = 0 lsp.start() lsp.client().request("workspace/executeCommand", { command = "zk." .. cmd, arguments = { path or resolve_notebook_dir(bufnr), - args, + options, }, }, function(err, res) assert(not err, tostring(err)) @@ -33,26 +54,38 @@ local function execute_command(path, cmd, args, cb) end, bufnr) end ---- https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zkindex -function M.index(path, args, cb) - execute_command(path, "index", args, cb) +---@param path? string path to explicitly specify the notebook +---@param options table additional options +---@param cb function callback function +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zkindex +function M.index(path, options, cb) + execute_command("index", path, options, cb) end ---- https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zknew -function M.new(path, args, cb) - execute_command(path, "new", args, cb) +---@param path? string path to explicitly specify the notebook +---@param options table additional options +---@param cb function callback function +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zknew +function M.new(path, options, cb) + execute_command("new", path, options, cb) end ---- https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist -function M.list(path, args, cb) - execute_command(path, "list", args, cb) +---@param path? string path to explicitly specify the notebook +---@param options table additional options +---@param cb function callback function +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist +function M.list(path, options, cb) + execute_command("list", path, options, cb) end M.tag = {} ---- https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zktaglist -function M.tag.list(path, args, cb) - execute_command(path, "tag.list", args, cb) +---@param path? string path to explicitly specify the notebook +---@param options table additional options +---@param cb function callback function +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zktaglist +function M.tag.list(path, options, cb) + execute_command("tag.list", path, options, cb) end return M diff --git a/lua/zk/config.lua b/lua/zk/config.lua index 01824bc..59db2e0 100644 --- a/lua/zk/config.lua +++ b/lua/zk/config.lua @@ -3,14 +3,14 @@ local M = {} M.defaults = { create_user_commands = true, lsp = { - auto_attach = { - enabled = true, - filetypes = { "markdown" }, - }, config = { cmd = { "zk", "lsp" }, name = "zk", }, + auto_attach = { + enabled = true, + filetypes = { "markdown" }, + }, }, } diff --git a/lua/zk/lsp.lua b/lua/zk/lsp.lua index af4e5fd..9da89c7 100644 --- a/lua/zk/lsp.lua +++ b/lua/zk/lsp.lua @@ -4,21 +4,22 @@ local client_id = nil local M = {} ---- Starts an LSP client if necessary +---Starts an LSP client if necessary function M.start() if not client_id then client_id = vim.lsp.start_client(config.options.lsp.config) end end ---- Starts an LSP client if necessary, and attaches the given buffer. +---Starts an LSP client if necessary, and attaches the given buffer. +---@param bufnr number function M.buf_add(bufnr) bufnr = bufnr or 0 M.start() vim.lsp.buf_attach_client(bufnr, client_id) end ---- Stops the LSP client managed by this plugin +---Stops the LSP client managed by this plugin function M.stop() local client = M.client() if client then @@ -27,7 +28,7 @@ function M.stop() client_id = nil end ---- Gets the LSP client managed by this plugin, might be nil +---Gets the LSP client managed by this plugin, might be nil function M.client() return vim.lsp.get_client_by_id(client_id) end diff --git a/lua/zk/util.lua b/lua/zk/util.lua index 758c1bb..aa14bce 100644 --- a/lua/zk/util.lua +++ b/lua/zk/util.lua @@ -15,18 +15,21 @@ function M.setup_lsp_auto_attach() vim.api.nvim_command(string.format("autocmd %s lua require'zk.util'.lsp_buf_auto_add(0)", trigger)) end -function M.is_notebook_path(path) - -- check that we got a match on the .zk root directory - return require("lspconfig.util").root_pattern(".zk")(path) ~= nil +---Checks whether the given path belongs to a notebook +---@param path string +---@return string? root +function M.notebook_root(path) + return require("lspconfig.util").root_pattern(".zk")(path) end ---- NOTE: No need to manually call this. Automatically called via an |autocmd| if lsp.auto_attach is enabled. +---Automatically called via an |autocmd| if lsp.auto_attach is enabled. +---@param bufnr number function M.lsp_buf_auto_add(bufnr) if vim.api.nvim_buf_get_option(bufnr, "buftype") == "nofile" then return end - if not M.is_notebook_path(vim.api.nvim_buf_get_name(bufnr)) then + if not M.notebook_root(vim.api.nvim_buf_get_name(bufnr)) then return end