diff --git a/README.md b/README.md index eac47d0..a70486a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,157 @@ # zk-nvim -Neovim extension for zk +Neovim extension for [zk](https://github.com/mickael-menu/zk). + +## Install + +Using [packer.nvim](https://github.com/wbthomason/packer.nvim) +```lua +use { + "mickael-menu/zk-nvim", + requires = { "neovim/nvim-lspconfig" } +} +-- Telescope is optional +use { + 'nvim-telescope/telescope.nvim', + requires = { {'nvim-lua/plenary.nvim'} } +} +``` + +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 +``` + +## Setup +```lua +require("zk").setup() +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 +```lua +require("zk").setup({ + 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" }, + name = "zk", + -- init_options = ... + -- on_attach = ... + -- etc, see `:h vim.lsp.start_client()` + }, + }, +}) +``` + +## Commands + +```vim +:ZkIndex +:ZkNew [] +``` +or via Lua +```lua +require("zk").index(path, args) -- path and args are optional +require("zk").new(path, args) -- path and args are optional +``` + +### Telescope + +```vim +:Telescope zk notes +:Telescope zk backlinks +:Telescope zk links +:Telescope zk related +:Telescope zk tags +``` +or via Lua +```lua +require('telescope').extensions.zk.notes() +require('telescope').extensions.zk.backlinks() +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'})`. + +## 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. + +```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) + -- 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) + file_path = res.path + -- do something with the new file path +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) + -- 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) + -- do something with the tags +end) +``` + +## Example Mappings +```lua +vim.api.nvim_set_keymap( + "n", + "zn", + "lua require('telescope').extensions.zk.notes()", + { noremap = true } +) + +vim.api.nvim_set_keymap( + "n", + "zb", + "lua require('telescope').extensions.zk.backlinks()", + { noremap = true } +) + +vim.api.nvim_set_keymap( + "n", + "zl", + "lua require('telescope').extensions.zk.links()", + { noremap = true } +) + +vim.api.nvim_set_keymap( + "n", + "zt", + "lua require('telescope').extensions.zk.tags()", + { noremap = true } +) +``` diff --git a/lua/telescope/_extensions/zk.lua b/lua/telescope/_extensions/zk.lua new file mode 100644 index 0000000..e9fb627 --- /dev/null +++ b/lua/telescope/_extensions/zk.lua @@ -0,0 +1,52 @@ +local util = require("telescope.zk.util") +local zk = require("zk") + +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) + util.show_note_picker(opts, notes) + end) +end + +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) +end + +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) +end + +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) +end + +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) + util.show_tag_picker(opts, tags, function(selected_tags) + zk.api.list(opts.path, util.wrap_note_args({ tags = selected_tags }), function(notes) + opts.prompt_title = "Zk Notes for tag(s) " .. vim.inspect(selected_tags) + util.show_note_picker(opts, notes) + end) + end) + end) +end + +return require("telescope").register_extension({ + exports = { + notes = show_notes, + backlinks = show_backlinks, + links = show_links, + related = show_related, + tags = show_tags, + }, +}) diff --git a/lua/telescope/zk/util.lua b/lua/telescope/zk/util.lua new file mode 100644 index 0000000..6cef6b8 --- /dev/null +++ b/lua/telescope/zk/util.lua @@ -0,0 +1,113 @@ +local pickers = require("telescope.pickers") +local finders = require("telescope.finders") +local conf = require("telescope.config").values +local actions = require("telescope.actions") +local action_state = require("telescope.actions.state") +local action_utils = require("telescope.actions.utils") +local putils = require("telescope.previewers.utils") +local entry_display = require("telescope.pickers.entry_display") +local previewers = require("telescope.previewers") + +local M = {} + +function M.wrap_note_args(opts) + return vim.tbl_deep_extend( + "force", + { select = { "title", "absPath", "rawContent" }, sort = { "created" } }, + opts or {} + ) +end + +function M.wrap_tag_args(opts) + return vim.tbl_deep_extend("force", { sort = { "note-count" } }, opts or {}) +end + +function M.create_note_entry_maker(_) + return function(note) + return { + value = note, + path = note.absPath, + display = note.title, + ordinal = note.title, + } + end +end + +function M.create_tag_entry_maker(opts) + return function(tag) + local displayer = entry_display.create({ + separator = " ", + items = { + { width = opts.note_count_width or 4 }, + { remaining = true }, + }, + }) + local make_display = function(e) + return displayer({ + { e.value.note_count, "TelescopeResultsNumber" }, + e.value.name, + }) + end + return { + value = tag, + display = make_display, + ordinal = tag.name, + } + end +end + +function M.make_note_previewer() + return previewers.new_buffer_previewer({ + define_preview = function(self, entry) + vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, vim.split(entry.value.rawContent, "\n")) + putils.highlighter(self.state.bufnr, "markdown") + end, + }) +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({ + results = notes, + entry_maker = M.create_note_entry_maker(opts), + }), + 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({ + results = tags, + entry_maker = M.create_tag_entry_maker(opts), + }), + sorter = conf.generic_sorter(opts), + attach_mappings = function(prompt_bufnr, _) + actions.select_default:replace(function() + local selection = {} + action_utils.map_selections(prompt_bufnr, function(entry, _) + table.insert(selection, entry.value.name) + end) + + if vim.tbl_isempty(selection) then + selection = { action_state.get_selected_entry().value.name } + end + + 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 new file mode 100644 index 0000000..544bb2c --- /dev/null +++ b/lua/zk.lua @@ -0,0 +1,34 @@ +local util = require("zk.util") +local config = require("zk.config") + +local M = {} + +M.api = require("zk.api") + +M.lsp = require("zk.lsp") + +function M.setup(options) + config.options = vim.tbl_deep_extend("force", config.defaults, options or {}) + if config.options.lsp.auto_attach.enabled then + util.setup_lsp_auto_attach() + end +end + +-- Commands + +function M.index(path, args) + M.api.index(path, args, function(stats) + vim.notify(vim.inspect(stats)) + end) +end + +function M.new(path, args) + M.api.new(path, args, function(res) + vim.cmd("edit " .. res.path) + end) +end + +vim.cmd("command! ZkIndex lua require('zk').index()") +vim.cmd("command! -nargs=? ZkNew lua require('zk').new(nil, { dir = [=[]=]})") + +return M diff --git a/lua/zk/api.lua b/lua/zk/api.lua new file mode 100644 index 0000000..90cd71f --- /dev/null +++ b/lua/zk/api.lua @@ -0,0 +1,58 @@ +local lsp = require("zk.lsp") +local util = require("zk.util") + +local M = {} + +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 + if path == "" then + path = vim.fn.getcwd(0) + 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 + end + return path +end + +local function execute_command(path, cmd, args, cb) + local bufnr = 0 + lsp.start() + lsp.client().request("workspace/executeCommand", { + command = "zk." .. cmd, + arguments = { + path or resolve_notebook_dir(bufnr), + args, + }, + }, function(err, res) + assert(not err, tostring(err)) + if res and cb then + cb(res) + end + 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) +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) +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) +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) +end + +return M diff --git a/lua/zk/config.lua b/lua/zk/config.lua new file mode 100644 index 0000000..8aa42ef --- /dev/null +++ b/lua/zk/config.lua @@ -0,0 +1,18 @@ +local M = {} + +M.defaults = { + lsp = { + auto_attach = { + enabled = true, + filetypes = { "markdown" }, + }, + config = { + cmd = { "zk", "lsp" }, + name = "zk", + }, + }, +} + +M.options = M.defaults + +return M diff --git a/lua/zk/lsp.lua b/lua/zk/lsp.lua new file mode 100644 index 0000000..af4e5fd --- /dev/null +++ b/lua/zk/lsp.lua @@ -0,0 +1,35 @@ +local config = require("zk.config") + +local client_id = nil + +local M = {} + +--- 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. +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 +function M.stop() + local client = M.client() + if client then + client.stop() + end + client_id = nil +end + +--- Gets the LSP client managed by this plugin, might be nil +function M.client() + return vim.lsp.get_client_by_id(client_id) +end + +return M diff --git a/lua/zk/util.lua b/lua/zk/util.lua new file mode 100644 index 0000000..758c1bb --- /dev/null +++ b/lua/zk/util.lua @@ -0,0 +1,36 @@ +local lsp = require("zk.lsp") +local config = require("zk.config") + +local M = {} + +function M.setup_lsp_auto_attach() + --- NOTE: modified version of code in nvim-lspconfig + local trigger + local filetypes = config.options.lsp.auto_attach.filetypes + if filetypes then + trigger = "FileType " .. table.concat(filetypes, ",") + else + trigger = "BufReadPost *" + end + 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 +end + +--- NOTE: No need to manually call this. Automatically called via an |autocmd| if lsp.auto_attach is enabled. +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 + return + end + + lsp.buf_add(bufnr) +end + +return M diff --git a/selene.toml b/selene.toml new file mode 100644 index 0000000..7312a91 --- /dev/null +++ b/selene.toml @@ -0,0 +1 @@ +std="vim" diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..0435f67 --- /dev/null +++ b/stylua.toml @@ -0,0 +1,2 @@ +indent_type = "Spaces" +indent_width = 2 diff --git a/vim.toml b/vim.toml new file mode 100644 index 0000000..fa8bcb4 --- /dev/null +++ b/vim.toml @@ -0,0 +1,6 @@ +[selene] +base = "lua51" +name = "vim" + +[vim] +any = true