diff --git a/README.md b/README.md index 70f85bd..80d4174 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,42 @@ # zk-nvim -Neovim extension for [zk](https://github.com/mickael-menu/zk). +Neovim extension for the [`zk`](https://github.com/mickael-menu/zk) plain text note-taking assistant. -## Install +## Installation -Using [packer.nvim](https://github.com/wbthomason/packer.nvim) -```lua -use { - "mickael-menu/zk-nvim", - requires = { "neovim/nvim-lspconfig" } -} +This plugin requires Neovim v0.6.0 or later. --- Telescope is optional -use { - 'nvim-telescope/telescope.nvim', - requires = { {'nvim-lua/plenary.nvim'} } -} +Via [packer.nvim](https://github.com/wbthomason/packer.nvim) +```lua +use("mickael-menu/zk-nvim") ``` -Using [vim-plug](https://github.com/junegunn/vim-plug) +Via [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 ``` +To get the best experience, it's recommended to also install either [Telescope](https://github.com/nvim-telescope/telescope.nvim) or [fzf](https://github.com/junegunn/fzf). + ## Setup + +> :warning: This plugin will setup and start the LSP server for you, do *not* call `require("lspconfig").zk.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** +**The default configuration** ```lua require("zk").setup({ - -- create user commands such as :ZkNew - create_user_commands = true, + -- can be "telescope", "fzf" or "select" (`vim.ui.select`) + -- it's recommended to use "telescope" or "fzf" + picker = "select", lsp = { -- `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()` }, @@ -58,6 +50,9 @@ require("zk").setup({ }) ``` +Note that the `setup` function will not add any key mappings for you. +If you want to add key mappings, see the [example mappings](#example-mappings). + ### 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), @@ -70,143 +65,239 @@ It is worth noting that for some notebook commands you can explicitly specify a An explicitly provided path will always take precedence and override the automatic notebook discovery. However, this is always optional, and usually not necessary. -## Commands +## Getting Started + +After you have installed the plugin and added the setup code to your config, you are good to go. If you are not familiar with `zk`, we recommend you to also read the [`zk` docs](https://github.com/mickael-menu/zk/tree/main/docs). + +When using the default config, the `zk` LSP client will automatically attach itself to buffers inside your notebook and provide capabilities like completion, hover and go-to-definition; see https://github.com/mickael-menu/zk/issues/22 for a full list of what is supported. + +Try out different [commands](#built-in-commands) such as `:ZkNotes` or `:ZkNew`, see what they can do, and learn as you go. + +## Built-in Commands -### VimL ```vim " Indexes the notebook " params " (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zkindex -:ZkIndex [] +:ZkIndex [{options}] +``` -" Creates a new note +```vim +" Creates and edits a new note " params " (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zknew -:ZkNew [] +:ZkNew [{options}] +``` +```vim " Creates a new note and uses the last visual selection as the title while replacing the selection with a link to the new note " params " (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zknew -:ZkNewLink [] +:'<,'>ZkNewFromTitleSelection [{options}] +``` -" Opens a Telescope picker +```vim +" Creates a new note and uses the last visual selection as the content while replacing the selection with a link to the new note +" params +" (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zknew +:'<,'>ZkNewFromContentSelection [{options}] +``` + +```vim +" cd into the notebook root +" params +" (optional) additional options +:ZkCd [{options}] +``` + +```vim +" Opens a notes picker " params " (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist -:ZkList [] +:ZkNotes [{options}] +``` -" Opens a Telescope picker +```vim +" Opens a notes picker for the backlinks of the current buffer +" params +" (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist +:ZkBacklinks [{options}] +``` + +```vim +" Opens a notes picker for the outbound links of the current buffer +" params +" (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist +:ZkLinks [{options}] +``` + +```vim +" Opens a notes picker, filters for notes that match the text in the last visual selection +" params +" (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist +:'<,'>ZkMatch [{options}] +``` + +```vim +" Opens a notes picker, filters for notes with the selected tags " params " (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zktaglist -:ZkTagList [] +:ZkTags [{options}] ``` -where `options` can be any valid *Lua* expression that evaluates to a table. + +The `options` parameter can be any valid *Lua* expression that evaluates to a table. +For a list of available options, refer to the [`zk` docs](https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#custom-commands). +In addition, `options.notebook_path` can be used to explicitly specify a notebook by providing a path to any file or directory within the notebook; see [Notebook Directory Discovery](#notebook-directory-discovery). *Examples:* ```vim :ZkNew { dir = "daily", date = "yesterday" } -:ZkList { createdAfter = "3 days ago", tags = { "work" } } -:'<,'>ZkNewLink " this will use your last visual mode selection. Note that you *must* call this command with the '<,'> range. +:ZkNotes { createdAfter = "3 days ago", tags = { "work" } } +:'<,'>ZkNewFromTitleSelection " this will use your last visual mode selection. Note that you *must* call this command with the '<,'> range. +:ZkCd ``` -### Lua -```lua ----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) +--- +**Via Lua** ----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) +You can access the underlying Lua function of a commands, with `require("zk.commands").get`. ----Creates a new note and uses the last visual selection as the title while replacing the selection with a link to the new note --- ----@param path? string path to explicitly specify the notebook ----@param options table additional options -require("zk").new_link(path, options) +*Examples:* +```lua +require("zk.commands").get("ZkNew")({ dir = "daily" }) +require("zk.commands").get("ZkNotes")({ createdAfter = "3 days ago", tags = { "work" } }) +require("zk.commands").get("ZkNewFromTitleSelection")() -- this will use your last visual mode selection +``` ----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) +## Custom Commands ----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) +```lua +---A thin wrapper around `vim.api.nvim_add_user_command` which parses the `params.args` of the command as a Lua table and passes it on to `fn`. +---@param name string +---@param fn function +---@param opts? table {needs_selection} makes sure the command is called with a range +---@see vim.api.nvim_add_user_command +require("zk.commands").add(name, fn, opts) ``` -*Examples:* +*Example 1:* + +Let us add a custom `:ZkOrphans` command that will list all notes that are orphans, i.e. not referenced by any other note. + ```lua -require("zk").new(nil, { dir = "daily" }) -require("zk").list(nil, { createdAfter = "3 days ago", tags = { "work" } }) -require("zk").new_link() -- this will use your last visual mode selection +local zk = require("zk") +local commands = require("zk.commands") + +commands.add("ZkOrphans", function(options) + options = vim.tbl_extend("force", { orphan = true }, options or {}) + zk.edit(options, { title = "Zk Orphans" }) +end) ``` +This adds the `:ZkOrphans [{options}]` vim user command, which accepts an `options` Lua table as an argument. +We can execute it like this `:ZkOrphans { tags = "work" }` for example. -As you can see, the `path` is optional, and can usually be omitted; see [Notebook Directory Discovery](#notebook-directory-discovery). +> Note: The `zk.edit` function is from the [high-level API](#high-level-api), which also contains other functions that might be useful for your custom commands. -### Telescope -```vim -:Telescope zk notes -:Telescope zk orphans -:Telescope zk backlinks -:Telescope zk links -:Telescope zk related -:Telescope zk tags +*Example 2:* + +Chances are that this will not be our only custom command following this pattern. +So let's also add a `:ZkRecents` command and make the pattern a bit more reusable. + +```lua +local zk = require("zk") +local commands = require("zk.commands") + +local function make_edit_fn(defaults, picker_options) + return function(options) + options = vim.tbl_extend("force", defaults, options or {}) + zk.edit(options, picker_options) + end +end + +commands.add("ZkOrphans", make_edit_fn({ orphan = true }, { title = "Zk Orphans" })) +commands.add("ZkRecents", make_edit_fn({ createdAfter = "2 weeks ago" }, { title = "Zk Recents" })) ``` -or via Lua + +## High-level API + +The high-level API is inspired by the commands provided by the `zk` CLI tool; see `zk --help`. +It's mainly used for the implementation of built-in and custom commands. + ```lua -require('telescope').extensions.zk.notes() -require('telescope').extensions.zk.orphans() -require('telescope').extensions.zk.backlinks() -require('telescope').extensions.zk.links() -require('telescope').extensions.zk.related() -require('telescope').extensions.zk.tags() +---Cd into the notebook root +-- +---@param options? table +require("zk").cd(options) ``` -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, specifying a `path` is optional, and is usually not necessary; see [Notebook Directory Discovery](#notebook-directory-discovery). +```lua +---Creates and edits a new note +-- +---@param options? table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zknew +require("zk").new(options) +``` -You can even pass the same additional options to the Telescope pickers as described in [list and tag list commands](#commands). +```lua +---Indexes the notebook +-- +---@param options? table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zkindex +require("zk").index(options) +``` -*Example VimL:* -```vim -:Telescope zk notes createdAfter=3\ days\ ago +```lua +---Opens a notes picker, and calls the callback with the selection +-- +---@param options? table additional options +---@param picker_options? table options for the picker +---@param cb function +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist +---@see zk.ui.pick_notes +require("zk").pick_notes(options, picker_options, cb) ``` -*Example Lua:* ```lua -require('telescope').extensions.zk.notes({ createdAfter = "3 days ago", tags = { "work" } }) +---Opens a tags picker, and calls the callback with the selection +-- +---@param options? table additional options +---@param picker_options? table options for the picker +---@param cb function +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zktaglist +---@see zk.ui.pick_tags +require("zk").pick_tags(options, picker_options, cb) ``` -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. +```lua +---Opens a notes picker, and edits the selected notes +-- +---@param options? table additional options +---@param picker_options? table options for the picker +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist +---@see zk.ui.pick_notes +require("zk").edit(options, picker_options) +``` ## API -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. +The functions in the API module give you maximum flexibility and provide only a thin Lua friendly layer around `zk`'s LSP 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 options are optional +---@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 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 options are optional +---@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 require("zk").api.new(path, options, function(res) file_path = res.path -- do something with the new file path @@ -214,75 +305,117 @@ end) ``` ```lua --- https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist --- path is optional, options.select is required --- options = { select = { "title", "absPath", "rawContent" }, sort = { "created" } } +---@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 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 options are optional +---@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 require("zk").api.tag.list(path, options, function(tags) -- do something with the tags end) ``` +## Pickers + +Used by the [high-level API](#high-level-api) to display the results of the [API](#api). + +```lua +---Opens a notes picker +-- +---@param notes list +---@param options? table containing {picker}, {title}, {multi_select} keys +---@param cb function +require("zk.ui").pick_notes(notes, options, cb) +``` + +```lua +---Opens a tags picker +-- +---@param tags list +---@param options? table containing {picker}, {title}, {multi_select} keys +---@param cb function +require("zk.ui").pick_tags(tags, options, cb) +``` + +```lua +---To be used in zk.api.list as the `selection` in the additional options table +-- +---@param options table the same options that are use for pick_notes +---@return table api selection +require("zk.ui").get_pick_notes_list_api_selection(options) +``` + ## Example Mappings ```lua +vim.api.nvim_set_keymap("n", "zc", "ZkNew", { noremap = true }) +vim.api.nvim_set_keymap("x", "zc", ":'<'>ZkNewFromTitleSelection", { noremap = true }) +vim.api.nvim_set_keymap("n", "zn", "ZkNotes", { noremap = true }) +vim.api.nvim_set_keymap("n", "zb", "ZkBacklinks", { noremap = true }) +vim.api.nvim_set_keymap("n", "zl", "ZkLinks", { noremap = true }) +vim.api.nvim_set_keymap("n", "zt", "ZkTags", { noremap = true }) +``` + +# Miscellaneous + +## Syntax Highlighting Tips + +These code snippets only work if you use Neovim's built-in Markdown syntax highlighting. + +*Proper syntax highlighting for Wikilinks.* [[This is a wiki link]]. +```vim +autocmd Filetype markdown syn region markdownWikiLink matchgroup=markdownLinkDelimiter start="\[\[" end="\]\]" contains=markdownUrl keepend oneline concealends +``` + +*Conceal support for normal Markdown links.* Overwrite the syntax regions like so +```vim +autocmd Filetype markdown syn region markdownLinkText matchgroup=markdownLinkTextDelimiter start="!\=\[\%(\%(\_[^][]\|\[\_[^][]*\]\)*]\%( \=[[(]\)\)\@=" end="\]\%( \=[[(]\)\@=" nextgroup=markdownLink,markdownId skipwhite contains=@markdownInline,markdownLineStart concealends +autocmd Filetype markdown syn region markdownLink matchgroup=markdownLinkDelimiter start="(" end=")" contains=markdownUrl keepend contained conceal +``` + +## nvim-lsp-installer +> Not recommended, instead install the [`zk`](https://github.com/mickael-menu/zk) CLI tool and make it available in your `$PATH`. + +If you insist to use nvim-lsp-installer for `zk`, the following code snippet should guide you on how to setup the `zk` server when installed via nvim-lsp-installer. + +```lua +require("nvim-lsp-installer").on_server_ready(function(server) + local opts = { + -- customize your options as usual + -- + -- on_attach = ... + -- etc, see `:h vim.lsp.start_client()` + } + if server.name == "zk" then + require("zk").setup({ + lsp = { + config = vim.tbl_extend("force", server:get_default_options(), opts), + }, + }) + else + server:setup(opts) + end +end) +``` --- Create notes / links - -vim.api.nvim_set_keymap( - "n", - "zc", - "lua require('zk').new()", - { noremap = true } -) - -vim.api.nvim_set_keymap( - "x", - "zc", - "lua require('zk').new_link()", - { noremap = true } -) - --- Show Telescope pickers - -vim.api.nvim_set_keymap( - "n", - "zn", - "lua require('telescope').extensions.zk.notes()", - { noremap = true } -) - -vim.api.nvim_set_keymap( - "n", - "zo", - "lua require('telescope').extensions.zk.orphans()", - { 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 } -) +## Telescope Plugin +> Not recommended, instead just use the [:ZkNotes command](#built-in-commands). + +It's possible (but unnecessary) to also load the notes picker as a telescope plugin. + +```lua +require("telescope").load_extension("zk") +``` + +```vim +:Telescope zk notes +:Telescope zk notes createdAfter=3\ days\ ago ``` diff --git a/doc/zk.txt b/doc/zk.txt new file mode 100644 index 0000000..6adcacf --- /dev/null +++ b/doc/zk.txt @@ -0,0 +1,433 @@ +zk.txt *zk.txt* Neovim extension for the `zk` plain text note-taking assistant. + + + +================================================================================ +CONTENTS *zk-contents* + +1. zk-nvim............................................................|zk-zk-nvim| + 1.1. Installation............................................|zk-installation| + 1.2. Setup..........................................................|zk-setup| + 1.2.1. Notebook Directory Discovery......|zk-notebook_directory_discovery| + 1.3. Getting Started......................................|zk-getting_started| + 1.4. Built-in Commands..................................|zk-built-in_commands| + 1.5. Custom Commands......................................|zk-custom_commands| + 1.6. High-level API........................................|zk-high-level_api| + 1.7. API..............................................................|zk-api| + 1.8. Pickers......................................................|zk-pickers| + 1.9. Example Mappings....................................|zk-example_mappings| +2. Miscellaneous................................................|zk-miscellaneous| + 2.1. Syntax Highlighting Tips....................|zk-syntax_highlighting_tips| + 2.2. nvim-lsp-installer................................|zk-nvim-lsp-installer| + 2.3. Telescope Plugin....................................|zk-telescope_plugin| + +================================================================================ +ZK-NVIM *zk-zk-nvim* + +Neovim extension for the `zk` (https://github.com/mickael-menu/zk) plain text note-taking assistant. + +-------------------------------------------------------------------------------- +INSTALLATION *zk-installation* + +This plugin requires Neovim v0.6.0 or later. + +Via packer.nvim (https://github.com/wbthomason/packer.nvim) +> + use("mickael-menu/zk-nvim") +< + +Via vim-plug (https://github.com/junegunn/vim-plug) +> + Plug "mickael-menu/zk-nvim" +< + +To get the best experience, it's recommended to also install either Telescope (https://github.com/nvim-telescope/telescope.nvim) or fzf (https://github.com/junegunn/fzf). + +-------------------------------------------------------------------------------- +SETUP *zk-setup* +> + :warning: This plugin will setup and start the LSP server for you, do not call `require("lspconfig").zk.setup()`. +< +> + require("zk").setup() +< + +The default configuration +> + require("zk").setup({ + -- can be "telescope", "fzf" or "select" (`vim.ui.select`) + -- it's recommended to use "telescope" or "fzf" + picker = "select", + lsp = { + -- `config` is passed to `vim.lsp.start_client(config)` + config = { + cmd = { "zk", "lsp" }, + name = "zk", + -- 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" }, + }, + }, + }) +< + +Note that the `setup` function will not add any key mappings for you. +If you want to add key mappings, see the example mappings (#example-mappings). + +NOTEBOOK DIRECTORY DISCOVERY *zk-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. + +-------------------------------------------------------------------------------- +GETTING STARTED *zk-getting_started* + +After you have installed the plugin and added the setup code to your config, you are good to go. If you are not familiar with `zk`, we recommend you to also read the `zk` docs (https://github.com/mickael-menu/zk/tree/main/docs). + +When using the default config, the `zk` LSP client will automatically attach itself to buffers inside your notebook and provide capabilities like completion, hover and go-to-definition; see https://github.com/mickael-menu/zk/issues/22 for a full list of what is supported. + +Try out different commands (#built-in-commands) such as `:ZkNotes` or `:ZkNew`, see what they can do, and learn as you go. + +-------------------------------------------------------------------------------- +BUILT-IN COMMANDS *zk-built-in_commands* +> + " Indexes the notebook + " params + " (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zkindex + :ZkIndex [{options}] +< +> + " Creates and edits a new note + " params + " (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zknew + :ZkNew [{options}] +< +> + " Creates a new note and uses the last visual selection as the title while replacing the selection with a link to the new note + " params + " (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zknew + :'<,'>ZkNewFromTitleSelection [{options}] +< +> + " Creates a new note and uses the last visual selection as the content while replacing the selection with a link to the new note + " params + " (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zknew + :'<,'>ZkNewFromContentSelection [{options}] +< +> + " cd into the notebook root + " params + " (optional) additional options + :ZkCd [{options}] +< +> + " Opens a notes picker + " params + " (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist + :ZkNotes [{options}] +< +> + " Opens a notes picker for the backlinks of the current buffer + " params + " (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist + :ZkBacklinks [{options}] +< +> + " Opens a notes picker for the outbound links of the current buffer + " params + " (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist + :ZkLinks [{options}] +< +> + " Opens a notes picker, filters for notes that match the text in the last visual selection + " params + " (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist + :'<,'>ZkMatch [{options}] +< +> + " Opens a notes picker, filters for notes with the selected tags + " params + " (optional) additional options, see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zktaglist + :ZkTags [{options}] +< + +The `options` parameter can be any valid Lua expression that evaluates to a table. +For a list of available options, refer to the `zk` docs (https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#custom-commands). +In addition, `options.notebook_path` can be used to explicitly specify a notebook by providing a path to any file or directory within the notebook; see Notebook Directory Discovery (#notebook-directory-discovery). + +Examples: +> + :ZkNew { dir = "daily", date = "yesterday" } + :ZkNotes { createdAfter = "3 days ago", tags = { "work" } } + :'<,'>ZkNewFromTitleSelection " this will use your last visual mode selection. Note that you *must* call this command with the '<,'> range. + :ZkCd +< + +-------------------------------------------------------------------------------- +Via Lua + +You can access the underlying Lua function of a commands, with `require("zk.commands").get`. + +Examples: +> + require("zk.commands").get("ZkNew")({ dir = "daily" }) + require("zk.commands").get("ZkNotes")({ createdAfter = "3 days ago", tags = { "work" } }) + require("zk.commands").get("ZkNewFromTitleSelection")() -- this will use your last visual mode selection +< + +-------------------------------------------------------------------------------- +CUSTOM COMMANDS *zk-custom_commands* +> + ---A thin wrapper around `vim.api.nvim_add_user_command` which parses the `params.args` of the command as a Lua table and passes it on to `fn`. + ---@param name string + ---@param fn function + ---@param opts? table {needs_selection} makes sure the command is called with a range + ---@see vim.api.nvim_add_user_command + require("zk.commands").add(name, fn, opts) +< + +Example 1: + +Let us add a custom `:ZkOrphans` command that will list all notes that are orphans, i.e. not referenced by any other note. +> + local zk = require("zk") + local commands = require("zk.commands") + commands.add("ZkOrphans", function(options) + options = vim.tbl_extend("force", { orphan = true }, options or {}) + zk.edit(options, { title = "Zk Orphans" }) + end) +< + +This adds the `:ZkOrphans [{options}]` vim user command, which accepts an `options` Lua table as an argument. +We can execute it like this `:ZkOrphans { tags = "work" }` for example. +> + Note: The `zk.edit` function is from the high-level API (#high-level-api), which also contains other functions that might be useful for your custom commands. +< + +Example 2: + +Chances are that this will not be our only custom command following this pattern. +So let's also add a `:ZkRecents` command and make the pattern a bit more reusable. +> + local zk = require("zk") + local commands = require("zk.commands") + local function make_edit_fn(defaults, picker_options) + return function(options) + options = vim.tbl_extend("force", defaults, options or {}) + zk.edit(options, picker_options) + end + end + commands.add("ZkOrphans", make_edit_fn({ orphan = true }, { title = "Zk Orphans" })) + commands.add("ZkRecents", make_edit_fn({ createdAfter = "2 weeks ago" }, { title = "Zk Recents" })) +< + +-------------------------------------------------------------------------------- +HIGH-LEVEL API *zk-high-level_api* + +The high-level API is inspired by the commands provided by the `zk` CLI tool; see `zk --help`. +It's mainly used for the implementation of built-in and custom commands. +> + ---Cd into the notebook root + -- + ---@param options? table + require("zk").cd(options) +< +> + ---Creates and edits a new note + -- + ---@param options? table additional options + ---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zknew + require("zk").new(options) +< +> + ---Indexes the notebook + -- + ---@param options? table additional options + ---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zkindex + require("zk").index(options) +< +> + ---Opens a notes picker, and calls the callback with the selection + -- + ---@param options? table additional options + ---@param picker_options? table options for the picker + ---@param cb function + ---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist + ---@see zk.ui.pick_notes + require("zk").pick_notes(options, picker_options, cb) +< +> + ---Opens a tags picker, and calls the callback with the selection + -- + ---@param options? table additional options + ---@param picker_options? table options for the picker + ---@param cb function + ---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zktaglist + ---@see zk.ui.pick_tags + require("zk").pick_tags(options, picker_options, cb) +< +> + ---Opens a notes picker, and edits the selected notes + -- + ---@param options? table additional options + ---@param picker_options? table options for the picker + ---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist + ---@see zk.ui.pick_notes + require("zk").edit(options, picker_options) +< + +-------------------------------------------------------------------------------- +API *zk-api* + +The functions in the API module give you maximum flexibility and provide only a thin Lua friendly layer around `zk`'s LSP API. +You can use it to write your own specialized functions for interacting with `zk`. +> + ---@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 + require("zk").api.index(path, options, function(stats) + -- do something with the stats + end) +< +> + ---@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 + require("zk").api.new(path, options, function(res) + file_path = res.path + -- do something with the new file path + end) +< +> + ---@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 + require("zk").api.list(path, options, function(notes) + -- do something with the notes + end) +< +> + ---@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 + require("zk").api.tag.list(path, options, function(tags) + -- do something with the tags + end) +< + +-------------------------------------------------------------------------------- +PICKERS *zk-pickers* + +Used by the high-level API (#high-level-api) to display the results of the API (#api). +> + ---Opens a notes picker + -- + ---@param notes list + ---@param options? table containing {picker}, {title}, {multi_select} keys + ---@param cb function + require("zk.ui").pick_notes(notes, options, cb) +< +> + ---Opens a tags picker + -- + ---@param tags list + ---@param options? table containing {picker}, {title}, {multi_select} keys + ---@param cb function + require("zk.ui").pick_tags(tags, options, cb) +< +> + ---To be used in zk.api.list as the `selection` in the additional options table + -- + ---@param options table the same options that are use for pick_notes + ---@return table api selection + require("zk.ui").get_pick_notes_list_api_selection(options) +< + +-------------------------------------------------------------------------------- +EXAMPLE MAPPINGS *zk-example_mappings* +> + vim.api.nvim_set_keymap("n", "zc", "ZkNew", { noremap = true }) + vim.api.nvim_set_keymap("x", "zc", ":'<'>ZkNewFromTitleSelection", { noremap = true }) + vim.api.nvim_set_keymap("n", "zn", "ZkNotes", { noremap = true }) + vim.api.nvim_set_keymap("n", "zb", "ZkBacklinks", { noremap = true }) + vim.api.nvim_set_keymap("n", "zl", "ZkLinks", { noremap = true }) + vim.api.nvim_set_keymap("n", "zt", "ZkTags", { noremap = true }) +< + +================================================================================ +MISCELLANEOUS *zk-miscellaneous* + +-------------------------------------------------------------------------------- +SYNTAX HIGHLIGHTING TIPS *zk-syntax_highlighting_tips* + +These code snippets only work if you use Neovim's built-in Markdown syntax highlighting. + +Proper syntax highlighting for Wikilinks. [[This is a wiki link]]. +> + autocmd Filetype markdown syn region markdownWikiLink matchgroup=markdownLinkDelimiter start="\[\[" end="\]\]" contains=markdownUrl keepend oneline concealends +< + +Conceal support for normal Markdown links. Overwrite the syntax regions like so +> + autocmd Filetype markdown syn region markdownLinkText matchgroup=markdownLinkTextDelimiter start="!\=\[\%(\%(\_[^][]\|\[\_[^][]*\]\)*]\%( \=[[(]\)\)\@=" end="\]\%( \=[[(]\)\@=" nextgroup=markdownLink,markdownId skipwhite contains=@markdownInline,markdownLineStart concealends + autocmd Filetype markdown syn region markdownLink matchgroup=markdownLinkDelimiter start="(" end=")" contains=markdownUrl keepend contained conceal +< + +-------------------------------------------------------------------------------- +NVIM-LSP-INSTALLER *zk-nvim-lsp-installer* +> + Not recommended, instead install the `zk` (https://github.com/mickael-menu/zk) CLI tool and make it available in your `$PATH`. +< + +If you insist to use nvim-lsp-installer for `zk`, the following code snippet should guide you on how to setup the `zk` server when installed via nvim-lsp-installer. +> + require("nvim-lsp-installer").on_server_ready(function(server) + local opts = { + -- customize your options as usual + -- + -- on_attach = ... + -- etc, see `:h vim.lsp.start_client()` + } + if server.name == "zk" then + require("zk").setup({ + lsp = { + config = vim.tbl_extend("force", server:get_default_options(), opts), + }, + }) + else + server:setup(opts) + end + end) +< + +-------------------------------------------------------------------------------- +TELESCOPE PLUGIN *zk-telescope_plugin* +> + Not recommended, instead just use the :ZkNotes command (#built-in-commands). +< + +It's possible (but unnecessary) to also load the notes picker as a telescope plugin. +> + require("telescope").load_extension("zk") +< +> + :Telescope zk notes + :Telescope zk notes createdAfter=3\ days\ ago +< + diff --git a/lua/telescope/_extensions/zk.lua b/lua/telescope/_extensions/zk.lua index daac99b..f1f4af1 100644 --- a/lua/telescope/_extensions/zk.lua +++ b/lua/telescope/_extensions/zk.lua @@ -1,68 +1,13 @@ -local util = require("telescope.zk.util") local zk = require("zk") ----@param opts table additional options +---@param opts? table additional options for zk, telescope options, all optional and in one table ---@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_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" }) - 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" }) - 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" }) - 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" }) - 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_options({}), function(tags) - util.show_tag_picker(opts, tags, function(selected_tags) - 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) - end) - end) + zk.edit(opts, { picker = "telescope", telescope = opts }) end return require("telescope").register_extension({ exports = { notes = show_notes, - orphans = show_orphans, - backlinks = show_backlinks, - links = show_links, - related = show_related, - tags = show_tags, }, }) diff --git a/lua/zk.lua b/lua/zk.lua index 33f52f0..69d9fb1 100644 --- a/lua/zk.lua +++ b/lua/zk.lua @@ -1,115 +1,136 @@ -local util = require("zk.util") +local lsp = require("zk.lsp") local config = require("zk.config") +local ui = require("zk.ui") +local api = require("zk.api") +local util = require("zk.util") local M = {} -M.api = require("zk.api") +local function 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'._lsp_buf_auto_add(0)", trigger)) +end -M.lsp = require("zk.lsp") +---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 util.notebook_root(vim.api.nvim_buf_get_name(bufnr)) then + return + end + + lsp.buf_add(bufnr) +end ---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 {}) if config.options.lsp.auto_attach.enabled then - util.setup_lsp_auto_attach() + setup_lsp_auto_attach() end - if config.options.create_user_commands then - vim.cmd([[ - " Core Commands - command! -nargs=? -complete=lua ZkIndex lua require('zk').index(nil, assert(loadstring('return ' .. ))()) - command! -nargs=? -complete=lua ZkNew lua require('zk').new(nil, assert(loadstring('return ' .. ))()) - command! -nargs=? -complete=lua ZkList lua require('zk').list(nil, assert(loadstring('return ' .. ))()) - command! -nargs=? -complete=lua ZkTagList lua require('zk').tag.list(nil, assert(loadstring('return ' .. ))()) - - " Convenience Commands - command! -range -nargs=? -complete=lua ZkNewLink lua assert( == 2, "ZkNewLink must be called with '<,'> range. Try :'<'>ZkNewLink"); require('zk').new_link(nil, assert(loadstring('return ' .. ))()) - ]]) - -- The definition of :ZkNewLink is kind of a hack. - -- The lua function that is called by ZkNewLink will always use the '<,'> marks to get the selection. - -- The only way that this command can be called that makes semantical sense is :'<,'>ZkNewLink. - end + require("zk.commands.builtin") end --- Core Commands - ----Indexes the notebook +---Cd into the notebook root -- ----@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) +---@param options? table +function M.cd(options) + options = options or {} + local notebook_path = options.notebook_path or util.resolve_notebook_path(0) + local root = util.notebook_root(notebook_path) + if root then + vim.cmd("cd " .. root) + end end ----Creates a new note +---Creates and edits a new note -- ----@param path? string path to explicitly specify the notebook ----@param options table additional options +---@param options? table additional options ---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zknew -function M.new(path, options) - -- neovim does not yet support window/showDocument, therefore we handle options.edit locally - if options and options.edit then - options.edit = nil -- nil means true in this context - end - M.api.new(path, options, function(res) +function M.new(options) + options = options or {} + api.new(options.notebook_path, options, function(res) if options and options.edit == false then return end + -- neovim does not yet support window/showDocument, therefore we handle options.edit locally vim.cmd("edit " .. res.path) end) end ----Opens a Telescope picker +---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#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) +---@param options? table additional options +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zkindex +function M.index(options) + options = options or {} + api.index(options.notebook_path, options, function(stats) + vim.notify(vim.inspect(stats)) + end) end -M.tag = {} +---Opens a notes picker, and calls the callback with the selection +-- +---@param options? table additional options +---@param picker_options? table options for the picker +---@param cb function +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist +---@see zk.ui.pick_notes +function M.pick_notes(options, picker_options, cb) + options = vim.tbl_extend( + "force", + { select = ui.get_pick_notes_list_api_selection(picker_options), sort = { "created" } }, + options or {} + ) + api.list(options.notebook_path, options, function(notes) + ui.pick_notes(notes, picker_options, cb) + end) +end ----Opens a Telescope picker +---Opens a tags picker, and calls the callback with the selection -- ----@param path? string path to explicitly specify the notebook ----@param options table additional options +---@param options? table additional options +---@param picker_options? table options for the picker +---@param cb function ---@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) +---@see zk.ui.pick_tags +function M.pick_tags(options, picker_options, cb) + options = vim.tbl_extend("force", { sort = { "note-count" } }, options or {}) + api.tag.list(options.notebook_path, options, function(tags) + ui.pick_tags(tags, picker_options, cb) + end) end --- Convenience Commands - ----Creates a new note and uses the last visual selection as the title while replacing the selection with a link to the new note +---Opens a notes picker, and edits the selected notes -- ----@param path? string path to explicitly specify the notebook ----@param options table additional options -function M.new_link(path, options) - local location = util.make_lsp_location() - local selected_text = util.get_text_in_range(location.range) - if not selected_text then - vim.notify("Selection not set", vim.log.levels.ERROR) - return - end - M.new(path, vim.tbl_extend("keep", options or {}, { insertLinkAtLocation = location, title = selected_text })) +---@param options? table additional options +---@param picker_options? table options for the picker +---@see https://github.com/mickael-menu/zk/blob/main/docs/editors-integration.md#zklist +---@see zk.ui.pick_notes +function M.edit(options, picker_options) + M.pick_notes(options, picker_options, function(notes) + if picker_options.multi_select == false then + notes = { notes } + end + for _, note in ipairs(notes) do + vim.cmd("e " .. note.absPath) + end + end) end return M diff --git a/lua/zk/api.lua b/lua/zk/api.lua index 4fbdd6f..a48801a 100644 --- a/lua/zk/api.lua +++ b/lua/zk/api.lua @@ -3,34 +3,6 @@ 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) - 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 = cwd - end - 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 - ---Executes the given command via LSP -- ---@param cmd string @@ -47,7 +19,7 @@ local function execute_command(cmd, path, options, cb) lsp.client().request("workspace/executeCommand", { command = "zk." .. cmd, arguments = { - path or resolve_notebook_dir(bufnr), + path or util.resolve_notebook_path(bufnr), options, }, }, function(err, res) @@ -59,7 +31,7 @@ local function execute_command(cmd, path, options, cb) end ---@param path? string path to explicitly specify the notebook ----@param options table additional options +---@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) @@ -67,7 +39,7 @@ function M.index(path, options, cb) end ---@param path? string path to explicitly specify the notebook ----@param options table additional options +---@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) @@ -85,7 +57,7 @@ end M.tag = {} ---@param path? string path to explicitly specify the notebook ----@param options table additional options +---@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) diff --git a/lua/zk/commands/builtin.lua b/lua/zk/commands/builtin.lua new file mode 100644 index 0000000..a045936 --- /dev/null +++ b/lua/zk/commands/builtin.lua @@ -0,0 +1,53 @@ +local zk = require("zk") +local util = require("zk.util") +local commands = require("zk.commands") + +commands.add("ZkIndex", zk.index) + +commands.add("ZkNew", zk.new) + +commands.add("ZkNewFromTitleSelection", function(options) + local location = util.get_lsp_location_from_selection() + local selected_text = util.get_text_in_range(location.range) + assert(selected_text ~= nil, "No selected text") + zk.new(vim.tbl_extend("force", { insertLinkAtLocation = location, title = selected_text }, options or {})) +end, { needs_selection = true }) + +commands.add("ZkNewFromContentSelection", function(options) + local location = util.get_lsp_location_from_selection() + local selected_text = util.get_text_in_range(location.range) + assert(selected_text ~= nil, "No selected text") + zk.new(vim.tbl_extend("force", { insertLinkAtLocation = location, content = selected_text }, options or {})) +end, { needs_selection = true }) + +commands.add("ZkCd", zk.cd) + +commands.add("ZkNotes", function(options) + zk.edit(options, { title = "Zk Notes" }) +end) + +commands.add("ZkBacklinks", function(options) + options = vim.tbl_extend("force", { linkTo = { vim.api.nvim_buf_get_name(0) } }, options or {}) + zk.edit(options, { title = "Zk Backlinks" }) +end) + +commands.add("ZkLinks", function(options) + options = vim.tbl_extend("force", { linkedBy = { vim.api.nvim_buf_get_name(0) } }, options or {}) + zk.edit(options, { title = "Zk Links" }) +end) + +commands.add("ZkMatch", function(options) + local selected_text = util.get_selected_text() + assert(selected_text ~= nil, "No selected text") + options = vim.tbl_extend("force", { match = selected_text }, options or {}) + zk.edit(options, { title = "Zk Notes matching " .. vim.inspect(selected_text) }) +end, { needs_selection = true }) + +commands.add("ZkTags", function(options) + zk.pick_tags(options, { title = "Zk Tags" }, function(tags) + tags = vim.tbl_map(function(v) + return v.name + end, tags) + zk.edit({ tags = tags }, { title = "Zk Notes for tag(s) " .. vim.inspect(tags) }) + end) +end) diff --git a/lua/zk/commands/init.lua b/lua/zk/commands/init.lua new file mode 100644 index 0000000..76c3e13 --- /dev/null +++ b/lua/zk/commands/init.lua @@ -0,0 +1,67 @@ +local M = {} + +local name_fn_map = {} + +-- NOTE: remove this once `vim.api.nvim_add_user_command` is officially released +M._name_command_map = {} + +-- NOTE: remove this helper once `vim.api.nvim_add_user_command` is officially released +local function nvim_add_user_command(name, command, opts) + if vim.api.nvim_add_user_command then + vim.api.nvim_add_user_command(name, command, opts) + else + assert(type(command) == "function", "Not supported in this version of Neovim.") + M._name_command_map[name] = command + vim.cmd(table.concat({ + "command" .. (opts.force and "!" or ""), + opts.range and "-range" or "", + opts.nargs and ("-nargs=" .. opts.nargs) or "", + opts.complete and ("-complete=" .. opts.complete) or "", + name, + string.format("lua require('zk.commands')._name_command_map['%s']({ args = , range = })", name), + }, " ")) + end +end + +-- NOTE: remove this helper once `vim.api.nvim_del_user_command` is officially released +local function nvim_del_user_command(name) + if vim.api.nvim_add_user_command then + vim.api.nvim_del_user_command(name) + else + M._name_command_map[name] = nil + vim.cmd("delcommand " .. name) + end +end + +---A thin wrapper around `vim.api.nvim_add_user_command` which parses the `params.args` of the command as a Lua table and passes it on to `fn`. +---@param name string +---@param fn function +---@param opts? table {needs_selection} makes sure the command is called with a range +---@see vim.api.nvim_add_user_command +function M.add(name, fn, opts) + opts = opts or {} + nvim_add_user_command(name, function(params) -- vim.api.nvim_add_user_command + if opts.needs_selection then + assert( + params.range == 2, + "Command needs a selection and must be called with '<,'> range. Try making a selection first." + ) + end + fn(loadstring("return " .. params.args)()) + end, { nargs = "?", force = true, range = opts.needs_selection, complete = "lua" }) + name_fn_map[name] = fn +end + +function M.get(name) + return name_fn_map[name] +end + +---Wrapper around `vim.api.nvim_del_user_command` +---@param name string +---@see vim.api.nvim_add_user_command +function M.del(name) + name_fn_map[name] = nil + nvim_del_user_command(name) -- vim.api.nvim_del_user_command +end + +return M diff --git a/lua/zk/config.lua b/lua/zk/config.lua index 59db2e0..79a4bce 100644 --- a/lua/zk/config.lua +++ b/lua/zk/config.lua @@ -1,7 +1,7 @@ local M = {} M.defaults = { - create_user_commands = true, + picker = "select", lsp = { config = { cmd = { "zk", "lsp" }, @@ -14,6 +14,6 @@ M.defaults = { }, } -M.options = M.defaults +M.options = M.defaults -- not necessary, but better code completion return M diff --git a/lua/zk/pickers/fzf.lua b/lua/zk/pickers/fzf.lua new file mode 100644 index 0000000..77cb385 --- /dev/null +++ b/lua/zk/pickers/fzf.lua @@ -0,0 +1,84 @@ +local M = {} + +local delimiter = "\x01 " + +-- we want can't do vim.fn["fzf#wrap"] because the sink/sinklist funcrefs +-- are reset to vim.NIL when they are converted to Lua +vim.cmd([[ + function! _fzf_wrap_and_run(...) + call fzf#run(call('fzf#wrap', a:000)) + endfunction +]]) + +M.note_picker_list_api_selection = { "title", "absPath" } + +function M.show_note_picker(notes, options, cb) + options = options or {} + vim.fn._fzf_wrap_and_run({ + source = vim.tbl_map(function(v) + return table.concat({ v.absPath, v.title }, delimiter) + end, notes), + options = vim.list_extend({ + "--delimiter=" .. delimiter, + "--tiebreak=index", + "--with-nth=2", + "--exact", + "--tabstop=4", + [[--preview=command -v bat 1>/dev/null 2>&1 && bat -p --color always {1} || cat {1}]], + "--preview-window=wrap", + options.title and "--header=" .. options.title or nil, + options.multi_select and "--multi" or nil, + }, options.fzf_options or {}), + sinklist = function(lines) + local notes_by_path = {} + for _, note in ipairs(notes) do + notes_by_path[note.absPath] = note + end + local selected_notes = vim.tbl_map(function(line) + local path = string.match(line, "([^" .. delimiter .. "]+)") + return notes_by_path[path] + end, lines) + if options.multi_select then + cb(selected_notes) + else + cb(selected_notes[1]) + end + end, + }) +end + +function M.show_tag_picker(tags, options, cb) + options = options or {} + vim.fn._fzf_wrap_and_run({ + source = vim.tbl_map(function(v) + return table.concat({ string.format("\x1b[31m%-4d\x1b[0m", v.note_count), v.name }, delimiter) + end, tags), + options = vim.list_extend({ + "--delimiter=" .. delimiter, + "--tiebreak=index", + "--nth=2", + "--exact", + "--tabstop=4", + "--ansi", + options.title and "--header=" .. options.title or nil, + options.multi_select and "--multi" or nil, + }, options.fzf or {}), + sinklist = function(lines) + local tags_by_name = {} + for _, tag in ipairs(tags) do + tags_by_name[tag.name] = tag + end + local selected_tags = vim.tbl_map(function(line) + local name = string.match(line, "([^" .. delimiter .. "]+)", 2) + return tags_by_name[name] + end, lines) + if options.multi_select then + cb(selected_tags) + else + cb(selected_tags[1]) + end + end, + }) +end + +return M diff --git a/lua/zk/pickers/select.lua b/lua/zk/pickers/select.lua new file mode 100644 index 0000000..f01ba2c --- /dev/null +++ b/lua/zk/pickers/select.lua @@ -0,0 +1,47 @@ +local M = {} + +M.note_picker_list_api_selection = { "title", "absPath" } + +function M.show_note_picker(notes, options, cb) + options = options or {} + local select_options = vim.tbl_extend("force", { + prompt = options.title, + format_item = function(item) + return item.title + end, + }, options.select or {}) + vim.ui.select(notes, select_options, function(item) + if not item then + -- user aborted + return + end + if options.multi_select then + cb({ item }) + else + cb(item) + end + end) +end + +function M.show_tag_picker(tags, options, cb) + options = options or {} + local select_options = vim.tbl_extend("force", { + prompt = "Zk Tags", + format_item = function(item) + return item.name + end, + }, options.select or {}) + vim.ui.select(tags, select_options, function(item) + if not item then + -- user aborted + return + end + if options.multi_select then + cb({ item }) + else + cb(item) + end + end) +end + +return M diff --git a/lua/telescope/zk/util.lua b/lua/zk/pickers/telescope.lua similarity index 50% rename from lua/telescope/zk/util.lua rename to lua/zk/pickers/telescope.lua index 2176d3c..5348ccc 100644 --- a/lua/telescope/zk/util.lua +++ b/lua/zk/pickers/telescope.lua @@ -10,17 +10,7 @@ local previewers = require("telescope.previewers") local M = {} -function M.wrap_note_options(opts) - return vim.tbl_deep_extend( - "force", - { select = { "title", "absPath", "rawContent" }, sort = { "created" } }, - opts or {} - ) -end - -function M.wrap_tag_options(opts) - return vim.tbl_deep_extend("force", { sort = { "note-count" } }, opts or {}) -end +M.note_picker_list_api_selection = { "title", "absPath", "rawContent" } function M.create_note_entry_maker(_) return function(note) @@ -65,40 +55,64 @@ function M.make_note_previewer() }) end -function M.show_note_picker(opts, notes) - opts = opts or {} - pickers.new(opts, { +function M.show_note_picker(notes, options, cb) + options = options or {} + local telescope_options = vim.tbl_extend("force", { prompt_title = options.title }, options.telescope or {}) + + pickers.new(telescope_options, { finder = finders.new_table({ results = notes, - entry_maker = M.create_note_entry_maker(opts), + entry_maker = M.create_note_entry_maker(options), }), - sorter = conf.file_sorter(opts), + sorter = conf.file_sorter(options), previewer = M.make_note_previewer(), + attach_mappings = function(prompt_bufnr) + actions.select_default:replace(function() + if options.multi_select then + local selection = {} + action_utils.map_selections(prompt_bufnr, function(entry, _) + table.insert(selection, entry.value) + end) + if vim.tbl_isempty(selection) then + selection = { action_state.get_selected_entry().value } + end + actions.close(prompt_bufnr) + cb(selection) + else + actions.close(prompt_bufnr) + cb(action_state.get_selected_entry().value) + end + end) + return true + end, }):find() end -function M.show_tag_picker(opts, tags, cb) - opts = opts or {} - pickers.new(opts, { +function M.show_tag_picker(tags, options, cb) + options = options or {} + local telescope_options = vim.tbl_extend("force", { prompt_title = options.title }, options.telescope or {}) + + pickers.new(telescope_options, { finder = finders.new_table({ results = tags, - entry_maker = M.create_tag_entry_maker(opts), + entry_maker = M.create_tag_entry_maker(options), }), - sorter = conf.generic_sorter(opts), + sorter = conf.generic_sorter(options), 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 } + if options.multi_select then + local selection = {} + action_utils.map_selections(prompt_bufnr, function(entry, _) + table.insert(selection, entry.value) + end) + if vim.tbl_isempty(selection) then + selection = { action_state.get_selected_entry().value } + end + actions.close(prompt_bufnr) + cb(selection) + else + cb(action_state.get_selected_entry().value) end - - actions.close(prompt_bufnr) - - cb(selection) end) return true end, diff --git a/lua/zk/root_pattern_util.lua b/lua/zk/root_pattern_util.lua new file mode 100644 index 0000000..01f67c3 --- /dev/null +++ b/lua/zk/root_pattern_util.lua @@ -0,0 +1,107 @@ +-- NOTE: everything in this module is copied from nvim-lspconfig (https://github.com/neovim/nvim-lspconfig) +-- NOTE: we need this util until the code from lspconfig is merged into core + +local vim = vim +local validate = vim.validate +local uv = vim.loop + +local M = {} + +-- Some path utilities +M.path = (function() + local is_windows = uv.os_uname().version:match 'Windows' + + local function exists(filename) + local stat = uv.fs_stat(filename) + return stat and stat.type or false + end + + local function is_fs_root(path) + if is_windows then + return path:match '^%a:$' + else + return path == '/' + end + end + + local function dirname(path) + local strip_dir_pat = '/([^/]+)$' + local strip_sep_pat = '/$' + if not path or #path == 0 then + return + end + local result = path:gsub(strip_sep_pat, ''):gsub(strip_dir_pat, '') + if #result == 0 then + if is_windows then + return path:sub(1, 2):upper() + else + return '/' + end + end + return result + end + + local function path_join(...) + return table.concat(vim.tbl_flatten { ... }, '/') + end + + -- Iterate the path until we find the rootdir. + local function iterate_parents(path) + local function it(_, v) + if v and not is_fs_root(v) then + v = dirname(v) + else + return + end + if v and uv.fs_realpath(v) then + return v, path + else + return + end + end + return it, path, path + end + + return { + exists = exists, + join = path_join, + iterate_parents = iterate_parents, + } +end)() + +function M.search_ancestors(startpath, func) + validate { func = { func, 'f' } } + if func(startpath) then + return startpath + end + local guard = 100 + for path in M.path.iterate_parents(startpath) do + -- Prevent infinite recursion if our algorithm breaks + guard = guard - 1 + if guard == 0 then + return + end + + if func(path) then + return path + end + end +end + +function M.root_pattern(...) + local patterns = vim.tbl_flatten { ... } + local function matcher(path) + for _, pattern in ipairs(patterns) do + for _, p in ipairs(vim.fn.glob(M.path.join(path, pattern), true, true)) do + if M.path.exists(p) then + return path + end + end + end + end + return function(startpath) + return M.search_ancestors(startpath, matcher) + end +end + +return M diff --git a/lua/zk/ui.lua b/lua/zk/ui.lua new file mode 100644 index 0000000..aa63309 --- /dev/null +++ b/lua/zk/ui.lua @@ -0,0 +1,42 @@ +local config = require("zk.config") + +local M = {} + +---Opens a notes picker +-- +---@param notes list +---@param options? table containing {picker}, {title}, {multi_select} keys +---@param cb function +function M.pick_notes(notes, options, cb) + options = vim.tbl_extend( + "force", + { title = "Zk Notes", picker = config.options.picker, multi_select = true }, + options or {} + ) + require("zk.pickers." .. options.picker).show_note_picker(notes, options, cb) +end + +---Opens a tags picker +-- +---@param tags list +---@param options? table containing {picker}, {title}, {multi_select} keys +---@param cb function +function M.pick_tags(tags, options, cb) + options = vim.tbl_extend( + "force", + { title = "Zk Tags", picker = config.options.picker, multi_select = true }, + options or {} + ) + require("zk.pickers." .. options.picker).show_tag_picker(tags, options, cb) +end + +---To be used in zk.api.list as the `selection` in the additional options table +-- +---@param options table the same options that are use for pick_notes +---@return table api selection +function M.get_pick_notes_list_api_selection(options) + options = vim.tbl_extend("force", { picker = config.options.picker }, options or {}) + return require("zk.pickers." .. options.picker).note_picker_list_api_selection +end + +return M diff --git a/lua/zk/util.lua b/lua/zk/util.lua index 7cfd853..8d8805a 100644 --- a/lua/zk/util.lua +++ b/lua/zk/util.lua @@ -1,50 +1,60 @@ -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 - ----Checks whether the given path belongs to a notebook ----@param path string +---Finds the root directory of the notebook of the given path +-- +---@param notebook_path string ---@return string? root -function M.notebook_root(path) - return require("lspconfig.util").root_pattern(".zk")(path) +function M.notebook_root(notebook_path) + return require("zk.root_pattern_util").root_pattern(".zk")(notebook_path) end ----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 +---Try to resolve a notebook path by checking the following locations in that order +---1. current buffer path +---2. current working directory +---3. `$ZK_NOTEBOOK_DIR` environment variable +--- +---Note that the path will not necessarily be the notebook root. +-- +---@param bufnr number? +---@return string? path inside a notebook +function M.resolve_notebook_path(bufnr) + local path = vim.api.nvim_buf_get_name(bufnr) + 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 = cwd end - - if not M.notebook_root(vim.api.nvim_buf_get_name(bufnr)) then - return + if not M.notebook_root(path) then + if not M.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 - - lsp.buf_add(bufnr) + -- at this point, the buffer either belongs to a notebook, or everything else failed + return path end -function M.make_lsp_location() +---Makes an LSP location object from the last selection in the current buffer. +-- +---@return table LSP location object +---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#location +function M.get_lsp_location_from_selection() local params = vim.lsp.util.make_given_range_params() params.uri = params.textDocument.uri params.textDocument = nil return params end ---- needed until https://github.com/neovim/neovim/pull/13896 is merged ----@param range table LSP range object +---Gets the text in the given range of the current buffer. +---Needed until https://github.com/neovim/neovim/pull/13896 is merged. +-- +---@param range table contains {start} and {end} tables with {line} and {character} values +---@return string? text in range function M.get_text_in_range(range) local A = range["start"] local B = range["end"] @@ -58,4 +68,27 @@ function M.get_text_in_range(range) return table.concat(lines, "\n") end +---Gets the most recently selected text of the current buffer. +---That is the text between the '<,'> marks. +---Note that these marks are only updated *after* leaving the visual mode. +-- +---@return string? selected text +function M.get_selected_text() + -- code adjusted from `vim.lsp.util.make_given_range_params` + -- we don't want to use character encoding offsets here + + local A = vim.api.nvim_buf_get_mark(0, "<") + local B = vim.api.nvim_buf_get_mark(0, ">") + -- convert to 0-index + A[1] = A[1] - 1 + B[1] = B[1] - 1 + if vim.o.selection ~= "exclusive" then + B[2] = B[2] + 1 + end + return M.get_text_in_range({ + start = { line = A[1], character = A[2] }, + ["end"] = { line = B[1], character = B[2] }, + }) +end + return M