diff --git a/autoload/SpaceVim/layers/zettelkasten.vim b/autoload/SpaceVim/layers/zettelkasten.vim new file mode 100644 index 000000000..f6cace398 --- /dev/null +++ b/autoload/SpaceVim/layers/zettelkasten.vim @@ -0,0 +1,44 @@ +"============================================================================= +" zettelkasten.vim --- zettelkasten layer for SpaceVim +" Copyright (c) 2016-2019 Wang Shidong & Contributors +" Author: Wang Shidong < wsdjeg@outlook.com > +" URL: https://spacevim.org +" License: GPLv3 +"============================================================================= + +function! SpaceVim#layers#zettelkasten#plugins() abort + let plugins = [] + call add(plugins, [g:_spacevim_root_dir . 'bundle/vim-zettelkasten', + \ { + \ 'merged' : 0, + \ }]) + return plugins +endfunction + +function! SpaceVim#layers#zettelkasten#health() abort + call SpaceVim#layers#zettelkasten#plugins() + return 1 +endfunction + +function! SpaceVim#layers#zettelkasten#loadable() abort + + return has('nvim') + +endfunction + +function! SpaceVim#layers#zettelkasten#config() abort + let g:_spacevim_mappings_space.m.z = {'name' : '+zettelkasten'} + call SpaceVim#mapping#space#def('nnoremap', ['m', 'z', 'n'], 'ZkNew', 'create-new-zettel-note', 1) +endfunction + +function! SpaceVim#layers#zettelkasten#set_variable(var) abort + let g:zettelkasten_directory = get(a:var, + \ 'zettel_dir', + \ '') +endfunction + +function! SpaceVim#layers#zettelkasten#get_options() abort + + return ['zettel_dir'] + +endfunction diff --git a/bundle/vim-zettelkasten/LICENSE b/bundle/vim-zettelkasten/LICENSE new file mode 100644 index 000000000..fdddb29aa --- /dev/null +++ b/bundle/vim-zettelkasten/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/bundle/vim-zettelkasten/README.md b/bundle/vim-zettelkasten/README.md new file mode 100644 index 000000000..312bcd5fa --- /dev/null +++ b/bundle/vim-zettelkasten/README.md @@ -0,0 +1,70 @@ +# vim-zettelkasten + +`vim-zettelkasten` is a [Zettelkasten](https://zettelkasten.de) note taking plugin. + +It is based on [zettelkasten.nvim@fe174666](https://github.com/Furkanzmc/zettelkasten.nvim/tree/fe1746666e27c2fcc0e60dc2786cb9983b994759) + +Using this plugin, you can: + +1. Create new notes with unique IDs (`:help :ZkNew`) + +2. List the places where a tag is used with `:tselect tag_name` or use Vim's own tag shortcuts for + navigation. + +3. Use `i_CTRL-X_CTRL-]` to get a list of all the tags in your notes. + +4. Get a completion list of note references. + +5. Use `K` command to display context for a note ID. + +6. Use `gf` to navigate to a reference. As long as your `:help path` option is set correctly, this + will work. + +There's no separate file type for zettelkasten. Markdown file type is used to extend the +functionality to make it easier to take notes. + +For the most up to date information, please do `:help zettelkasten.txt`. I won't be updating README +file for every single feature or update. You can also check out the +[wiki](https://github.com/Furkanzmc/zettelkasten.nvim/wiki) for tips and tricks on how to use +zettelkasten.nvim. + +# Configuration + +See `:help zettelkasten.txt` for more information. + +# TODO + +Potential ideas to implement in the future: + +- [ ] A graph view (Possible with an external CLI program.) +- [X] A sidebar (or preview window) to display the linked notes. +- [ ] Telescope support. It's a popular plugin so it'd be useful but I don't use Telescope so + contributions for this feature is most welcome! + +# Project Goals + +I started the project out of a bout of excitement for having discovered the Zettelkasten note +taking system. I've been looking for better ways to take notes and this system seems to fulfill my +needs. Since I love Vim, and Zettelkasten is a text based system (Which is what I love the most +about it), I decided to create a plugin immediately. + +My goal is not to turn this into a huge thing with custom pickers, and file types, and a gazillion +mapping and commands. My goal is to make use of the existing Vim options/mappings/features to +extend markdown file type so it's more convenient to navigate, discover, and write. + +As you can see from its initial state, the only thing you need to really know about this plugin is +the `:ZkNew` command. Everything else can be discovered as you are flexing your usual Vim muscles +(e. `gf`, `i_CTRL-X_CTRL-]`, `CTRL-]`). + +In true Vim philosophy, I also want to make it easier for people to extend this plugin to their own +needs. So, all the Lua API will be nicely designed so you can interface this plugin with others +(e.g `Telescope.nvim`) or create your own workflow easily. + +Please also see `:help zettelkasten.nvim-101` and `:help zettelkasten-philosophy`. + +# Related Projects + +- [zk-nvim](https://github.com/mickael-menu/zk-nvim) +- [zettel.vim](https://github.com/Aarleks/zettel.vim/) +- [telekasten.nvim](https://github.com/renerocksai/telekasten.nvim) +- [marty-oehme/zettelkasten.nvim](https://github.com/marty-oehme/zettelkasten.nvim) diff --git a/bundle/vim-zettelkasten/doc/zettelkasten.txt b/bundle/vim-zettelkasten/doc/zettelkasten.txt new file mode 100644 index 000000000..a6bfa2f2e --- /dev/null +++ b/bundle/vim-zettelkasten/doc/zettelkasten.txt @@ -0,0 +1,256 @@ +*zettelkasten.txt* vim-zettelkasten + +Origin Author: Furkan Uzumcu +Maintainer: Shidong Wang + +INTRODUCTION *vim-zettelkasten* *zettelkasten* +================================================================================ + +`vim-zettelkasten` is forked from `zettelkasten.nvim`. The goal of this plugin +is to make it easier to use the Zettelkasten note taking system: +https://zettelkasten.de/introduction/ + +It is meant to provide the following features in a way that is consistent with +Vim's own features: + +1. Note ID completion +2. ID generation +3. Jumping to references +4. Jumping to tags +5. Listing tags + +All relevant options are set automatically if they have not been set before. +If they were set, these are the options that you can use to get the best +experience. These options may change, make sure the check the filetype file: > + + setlocal tagfunc=v:lua.zettelkasten.tagfunc + setlocal isfname+=-,: + setlocal iskeyword+=:,- + setlocal suffixesadd+=.md + setlocal keywordprg=:ZkHover + setlocal completefunc=v:lua.zettelkasten.completefunc + +ZETTELKASTEN.NVIM 101 *zettelkasten.nvim-101* +================================================================================ + +The plugin does not introduce any new syntax for note taking. You can take +advantage of the markdown syntax with headers, tags, and links. You can refer +to a note by wrapping the note ID (i.e. the file name) with double angle +brackets. + +> + This note refers to [[2022-02-27-06-05-03]] which was created using + :ZkNew. + It can also refer to an [[existing_note]]. With this approach, you won't + be able to use `:help CTRL-]` to jump to the file buy you can still use + `:help gf` + +`zettelkasten.nvim` also assumes a single folder where all the notes reside. +If you need to organize things you can use `#tags` to group them or create a +note that will be used as a reference for other notes. + +PHILOSOPHY *zettelkasten-philosophy* +================================================================================ + +`zettelkasten.nvim` embraces Vim by trying its hardest to not create new +commands or mappings. Everything this plugin provides should ideally be +useable with existing Vim options, mappings, and commands. + +Where possible, Lua API should be exposed so others can extend this plugin's +functionality or create their own plugins on top of this one (e.g. telescope +pickers.). + +PREVIEWING NOTES *zettelkasten-preview-notes* +================================================================================ + +To review a note, you can either use |K| mapping to execute |keywordprg| or +use any of the preview window tag mappings to open the note in a preview +window. Note that when the preview window commands are used (e.g |CTRL-W }|), +|preview-window| is used. If you want to customize the preview window, use +`preview_command` when setting up the plugin and rely on |K| command. + + include ~ + + |include| commands can be used to see the references as well. You can use + `[I` to display all the lines that contain the reference under the cursor. + +MAPS *zettelkasten-maps* +================================================================================ + +Buffer Local ~ + + *zettelkasten-[I* +[I Lists the notes that reference the note ID under the cursor. Available + in markdown buffers and plugin specific buffers. + +COMMANDS *zettelkasten-commands* +================================================================================ + +Global ~ + +ZkNew *:ZkNew* + + Creates a new buffer with a markdown file type. If {notes_path} option was + set with |zettelkasten-setup|, it'll change the working directory. + + +ZkHover {-preview} {-return-lines} {note_id} *:ZkHover* + + Implements |keywordprg|. This command is set to |keywordprg| option if + |keywordprg| is not already set. + + If `-preview` is passed to the command, a preview window will be used to + show the note contents. + + If `-return-lines` can only be used when `-preview` is used. When this + argument is passed, this command will return the note content in a list + instead of using `preview_command`. + + Example: > + setlocal keywordprg=:ZkHover\ -preview + setlocal keywordprg=:ZkHover\ -preview\ -return-lines + +ZkBrowse *:ZkBrowse* + + Just a command that runs `edit zk://browser`. + In this buffer, you can use the same short cuts that you use to navigate + and open notes as in the markdown files. + +LUA *zettelkasten-lua* +================================================================================ + +setup({opts}) *zettelkasten-setup* + + Initialize the plugin. + + Parameters: ~ + {opts} (optional, table) + - {notes_path} (optional, string): Defaults to an empty string. If + provided, |:ZkNew| uses the notes path to set the working + directory. If this is not set. you need to make sure that your + working directory is your notes directory. Otherwise some + features may not work. + - {preview_command} (optional, string): The command to use to + preview a note. This will be used with |:ZkHover -preview| + command. The command must take one string argument as the name + of the note file. This can be used to configure + zettelkasten.nvim so the hover previews are (for example) shown + in a hover window. + - {browseformat} (optional, string): Used to format each line when + browsing the notes. Here's the supported values: + field meaning ~ + %f File name of the note. + %h Note's header. + %b Number of references to this note. + %r Number of notes this file references. + %t Tags in the note. + %d Note ID. + Default value is: `%f - %h [%r Refs] [%b B-Refs] %t` + +keyword_expr({cword}, {opts}) *zettelkasten-keyword-expr* + + Returns a table that contains the context for the note with the given ID + {cword}. + + Parameters: ~ + {cword} (string): The word under the cursor. If a note with the given + ID cannot be found, returns an empty table. + {opts} (optional, table) + - {preview_note} (optional, boolean): If set to true, + `preview_command` will be used to preview the note. + - {return_lines} (optional, boolean): Only used when + `preview_note` is set to `true`. When set, instead of using + `preview_command`, it returns the note contents from the + command. + + Returns: ~ + {table} + +tagfunc({pattern}, {flags}, {info}) *zettelkasten-tagfunc* + + Implements a |tagfunc|. This function is automatically set to the + |tagfunc| option by the plugin. + + Returns: ~ + {table} + +set_note_id({bufnr}) *zettelkasten-set-note-id* + + Prepends a note ID to the first line of the current buffer. It expects to + find a markdown header in the first line. + + Parameters: ~ + {bufnr} (number): Current buffer, or another note buffer. + +completefunc({base}) *zettelkasten-completefunc* + + Implements a |complete-function|. Given a {base} note ID or title, returns + a list of notes matching notes. + + Parameters: ~ + {base} (string): Note ID or title. + +show_back_references({cword}, {use_loclist}) *zettelkasten-show_back_references* + + Presents the list of notes that reference the note with {cword} ID. + If {use_loclist} set to true, location list will be used instead of + quickfix list. + +get_back_references({note_id}) *zettelkasten-get_back_references* + + Returns a list containg an item each describing a note that references + {note_id}. + + Each item in the return table contains these values: + + - {id} (string): The note ID that references {note_id} + - {linenr} (number): The line where the reference is found. + - {title} (string): The title of the note with {id} + - {file_name} (string): The file name of the note with {id} + +get_toc({note_id}, {format}) *zettelkasten-get_toc* + + Returns a table that contains a list of notes that refer to {note_id} + formatted according to {format}. + + You can use this function to insert all the notes that refer to a master + note. + + Parameters: ~ + {note_id} (string): Note ID. + {format} (optional, string) + The default format is `- [%h](%d)`. Only these format options are + supported. + + field meaning ~ + %f File name of the note. + %h Note's header. + %d Note ID. + + Returns: ~ + {table} + + Example: ~ + Insert Table of Contents with a Command: > + + vim.api.nvim_buf_add_user_command(0, "ZkInsertTOC", function(opts) + vim.api.nvim_buf_set_lines( + vim.api.nvim_get_current_buf(), + opts.line1, + opts.line2, + true, + require("zettelkasten").get_toc(opts.args) + ) + end, { + nargs = 1, + range = true, + }) + +ABOUT *zettelkasten-about* +================================================================================ + +Grab the latest version or report a bug on GitHub: + +https://github.com/Furkanzmc/zettelkasten.nvim + +vim:tw=80:colorcolumn=81:et:ft=help:norl: diff --git a/bundle/vim-zettelkasten/ftdetect/zettelkasten.lua b/bundle/vim-zettelkasten/ftdetect/zettelkasten.lua new file mode 100644 index 000000000..869222943 --- /dev/null +++ b/bundle/vim-zettelkasten/ftdetect/zettelkasten.lua @@ -0,0 +1,4 @@ +vim.cmd([[augroup zettelkasten_ft]]) +vim.cmd([[au!]]) +vim.cmd([[autocmd BufNewFile,BufRead zk://browser setlocal filetype=zkbrowser]]) +vim.cmd([[augroup END]]) diff --git a/bundle/vim-zettelkasten/ftplugin/markdown.lua b/bundle/vim-zettelkasten/ftplugin/markdown.lua new file mode 100644 index 000000000..4c264c081 --- /dev/null +++ b/bundle/vim-zettelkasten/ftplugin/markdown.lua @@ -0,0 +1,34 @@ +local zk = require("zettelkasten") + +if vim.opt_local.tagfunc:get() == "" then + vim.opt_local.tagfunc = "v:lua.zettelkasten.tagfunc" +end + +if vim.opt_local.completefunc:get() == "" then + vim.opt_local.completefunc = "v:lua.zettelkasten.completefunc" +end + +vim.opt.isfname:append(":") +vim.opt.isfname:append("-") +vim.opt_local.iskeyword:append(":") +vim.opt_local.iskeyword:append("-") +vim.opt_local.suffixesadd:append(".md") +vim.opt_local.errorformat = "%f:%l: %m" +vim.opt_local.include = "[[\\s]]" +vim.opt_local.define = "^# \\s*" + +if vim.opt_local.keywordprg:get() == "" then + vim.opt_local.keywordprg = ":ZkHover" +end + +if vim.fn.mapcheck("[I", "n") == "" then + vim.api.nvim_buf_set_keymap( + 0, + "n", + "[I", + 'lua require("zettelkasten").show_back_references(vim.fn.expand(""))', + { noremap = true, silent = true, nowait = true } + ) +end + +require("zettelkasten").add_hover_command() diff --git a/bundle/vim-zettelkasten/ftplugin/zkbrowser.lua b/bundle/vim-zettelkasten/ftplugin/zkbrowser.lua new file mode 100644 index 000000000..7f0a7718b --- /dev/null +++ b/bundle/vim-zettelkasten/ftplugin/zkbrowser.lua @@ -0,0 +1,55 @@ +if vim.b.did_ftp == true then + return +end + +vim.opt_local.cursorline = true +vim.opt_local.modifiable = true +vim.opt_local.buflisted = true +vim.opt_local.syntax = "zkbrowser" +vim.opt_local.buftype = "nofile" +vim.opt_local.swapfile = false +vim.opt_local.iskeyword:append(":") +vim.opt_local.iskeyword:append("-") +vim.opt_local.suffixesadd:append(".md") +vim.opt_local.errorformat = "%f:%l: %m" + +if vim.opt_local.keywordprg:get() == "" then + vim.opt_local.keywordprg = ":ZkHover -preview" +end + +if vim.opt_local.tagfunc:get() == "" then + vim.opt_local.tagfunc = "v:lua.zettelkasten.tagfunc" +end + +require("zettelkasten").add_hover_command() + +if vim.fn.mapcheck("[I", "n") == "" then + vim.api.nvim_buf_set_keymap( + 0, + "n", + "[I", + 'lua require("zettelkasten").show_back_references(vim.fn.expand(""))', + { noremap = true, silent = true, nowait = true } + ) +end + +local config = require("zettelkasten.config").get() +if config.notes_path ~= "" then + vim.cmd("lcd " .. config.notes_path) +end + +vim.api.nvim_create_autocmd({ "BufEnter" }, { + group = vim.api.nvim_create_augroup("zettelkasten_browser_events", { clear = true }), + buffer = vim.api.nvim_get_current_buf(), + callback = function(opts) + vim.opt_local.syntax = "" + vim.api.nvim_buf_set_lines( + 0, + vim.fn.line("$") - 1, + -1, + true, + require("zettelkasten").get_note_browser_content() + ) + vim.opt_local.syntax = "zkbrowser" + end, +}) diff --git a/bundle/vim-zettelkasten/lua/zettelkasten.lua b/bundle/vim-zettelkasten/lua/zettelkasten.lua new file mode 100644 index 000000000..823d7b8aa --- /dev/null +++ b/bundle/vim-zettelkasten/lua/zettelkasten.lua @@ -0,0 +1,314 @@ +local M = {} +local api = vim.api +local fn = vim.fn + +local log_levels = vim.log.levels +local log = require("zettelkasten.log") +local config = require("zettelkasten.config") +local formatter = require("zettelkasten.formatter") +local browser = require("zettelkasten.browser") + +local NOTE_ID_STRFTIME_FORMAT = "%Y-%m-%d-%H-%M-%S" + +local function set_qflist(lines, action, bufnr, use_loclist, what) + what = what or {} + local _, local_efm = pcall(vim.api.nvim_buf_get_option, bufnr, "errorformat") + what.efm = what.efm or local_efm + if use_loclist then + fn.setloclist(bufnr, lines, action, what) + else + fn.setqflist(lines, action, what) + end +end + +local function read_note(file_path, line_count) + local file = io.open(file_path, "r") + assert(file ~= nil) + assert(file:read(0) ~= nil) + + if line_count == nil then + return file:read("*all") + end + + local lines = {} + while #lines < line_count do + local line = file:read("*line") + if line == nil then + break + end + + table.insert(lines, line) + end + + return lines +end + +local function get_all_tags(lookup_tag) + if lookup_tag ~= nil and #lookup_tag > 0 then + lookup_tag = string.gsub(lookup_tag, "\\<", "") + lookup_tag = string.gsub(lookup_tag, "\\>", "") + end + + local tags = browser.get_tags() + if lookup_tag ~= nil and #lookup_tag > 0 then + tags = vim.tbl_filter(function(tag) + return string.match(tag.name, lookup_tag) ~= nil + end, tags) + end + + return tags +end + +local function generate_note_id() + return fn.strftime(NOTE_ID_STRFTIME_FORMAT) +end + +function M.completefunc(find_start, base) + if find_start == 1 and base == "" then + local pos = api.nvim_win_get_cursor(0) + local line = api.nvim_get_current_line() + local line_to_cursor = line:sub(1, pos[2]) + return fn.match(line_to_cursor, "\\k*$") + end + + local notes = vim.tbl_filter(function(note) + return string.match(note.title, base) + end, browser.get_notes()) + + local words = {} + for _, ref in ipairs(notes) do + table.insert(words, { + word = ref.id, + abbr = ref.title, + dup = 0, + empty = 0, + kind = "[zettelkasten]", + icase = 1, + }) + end + + return words +end + +function M.set_note_id(bufnr) + local first_line = vim.api.nvim_buf_get_lines(bufnr, 0, 1, true)[1] + local zk_id = generate_note_id() + if #zk_id > 0 then + first_line, _ = string.gsub(first_line, "# ", "") + api.nvim_buf_set_lines(bufnr, 0, 1, true, { "# " .. zk_id .. " " .. first_line }) + vim.cmd("file " .. zk_id .. ".md") + else + log.notify("There's already a note with the same ID.", log_levels.ERROR, {}) + end +end + +function M.tagfunc(pattern, flags, info) + local in_insert = string.match(flags, "i") ~= nil + local pattern_provided = pattern ~= "\\<\\k\\k" or pattern == "*" + local all_tags = {} + if pattern_provided then + all_tags = get_all_tags(pattern) + else + all_tags = get_all_tags() + end + + local tags = {} + for _, tag in ipairs(all_tags) do + table.insert(tags, { + name = string.gsub(tag.name, "#", ""), + filename = tag.file_name, + cmd = tostring(tag.linenr), + kind = "zettelkasten", + }) + end + + if not in_insert then + local notes = browser.get_notes() + for _, note in ipairs(notes) do + if string.find(note.id, pattern, 1, true) or not pattern_provided then + table.insert(tags, { + name = note.id, + filename = note.file_name, + cmd = "1", + kind = "zettelkasten", + }) + end + end + end + + if #tags > 0 then + return tags + end + + return nil +end + +function M.keyword_expr(word, opts) + if not word then + return {} + end + + opts = opts or {} + opts.preview_note = opts.preview_note or false + opts.return_lines = opts.return_lines or false + + local note = browser.get_note(word) + if note == nil then + log.notify("Cannot find note.", log_levels.ERROR, {}) + return {} + end + + local lines = {} + if opts.preview_note and not opts.return_lines then + vim.cmd(config.get().preview_command .. " " .. note.file_name) + elseif opts.preview_note and opts.return_lines then + vim.list_extend(lines, read_note(note.file_name)) + else + table.insert(lines, note.title) + end + + return lines +end + +function M.get_back_references(note_id) + local note = browser.get_note(note_id) + if note == nil then + return {} + end + + local title_cache = {} + local get_title = function(id) + if title_cache[id] ~= nil then + return title_cache[id] + end + + title_cache[id] = browser.get_note(id).title + return title_cache[id] + end + + local references = {} + for _, back_reference in ipairs(note.back_references) do + table.insert(references, { + id = back_reference.id, + linenr = back_reference.linenr, + title = back_reference.title, + file_name = back_reference.file_name, + }) + end + + return references +end + +function M.show_back_references(cword, use_loclist) + use_loclist = use_loclist or false + local references = M.get_back_references(cword) + if #references == 0 then + log.notify("No back references found.", log_levels.ERROR, {}) + return + end + + local lines = {} + for _, ref in ipairs(references) do + local line = {} + table.insert(line, fn.fnamemodify(ref.file_name, ":.")) + table.insert(line, ":") + table.insert(line, ref.linenr) + table.insert(line, ": ") + table.insert(line, ref.title) + + table.insert(lines, table.concat(line, "")) + end + + set_qflist( + {}, + " ", + vim.api.nvim_get_current_buf(), + use_loclist, + { title = "[[" .. cword .. "]] References", lines = lines } + ) + + if use_loclist then + vim.cmd([[botright lopen | wincmd p]]) + else + vim.cmd([[botright copen | wincmd p]]) + end +end + +function M.get_toc(note_id, format) + format = format or "- [%h](%d)" + local references = M.get_back_references(note_id) + local lines = {} + for _, note in ipairs(references) do + table.insert(lines, { + file_name = note.file_name, + id = note.id, + title = note.title, + }) + end + + return formatter.format(lines, format) +end + +function M.get_note_browser_content() + if config.get().notes_path == "" then + log.notify("'notes_path' option is required for note browsing.", log_levels.WARN, {}) + return {} + end + + local all_notes = browser.get_notes() + local lines = {} + for _, note in ipairs(all_notes) do + table.insert(lines, { + file_name = note.file_name, + id = note.id, + references = note.references, + back_references = note.back_references, + tags = note.tags, + title = note.title, + }) + end + + return formatter.format(lines, config.get().browseformat) +end + +function M.add_hover_command() + if fn.exists(":ZkHover") == 2 then + return + end + + vim.cmd( + [[command -buffer -nargs=1 ZkHover :lua require"zettelkasten"._internal_execute_hover_cmd()]] + ) +end + +function M._internal_execute_hover_cmd(args) + if args ~= nil and type(args) == "string" then + args = vim.split(args, " ", true) + else + args = {} + end + + local cword = "" + if #args == 1 then + cword = fn.expand("") + else + cword = args[#args] + end + + local lines = M.keyword_expr(cword, { + preview_note = vim.tbl_contains(args, "-preview"), + return_lines = vim.tbl_contains(args, "-return-lines"), + }) + if #lines > 0 then + log.notify(table.concat(lines, "\n"), log_levels.INFO, {}) + end +end + +function M.setup(opts) + opts = opts or {} + opts.notes_path = opts.notes_path or "" + + config._set(opts) +end + +return M diff --git a/bundle/vim-zettelkasten/lua/zettelkasten/browser.lua b/bundle/vim-zettelkasten/lua/zettelkasten/browser.lua new file mode 100644 index 000000000..f9d2c5e77 --- /dev/null +++ b/bundle/vim-zettelkasten/lua/zettelkasten/browser.lua @@ -0,0 +1,215 @@ +local M = {} +local fn = vim.fn + +local config = require("zettelkasten.config") + +local ZK_FULL_TITLE_PATTERN = "# %d+-%d+-%d+-%d+-%d+-%d+ .+" +local ZK_ID_PATTERN = "%d+-%d+-%d+-%d+-%d+-%d+" +local ZK_FILE_NAME_PATTERN = "%d+-%d+-%d+-%d+-%d+-%d+.md" + +local s_note_cache_with_file_path = {} +local s_note_cache_with_id = {} + +local function get_files(folder) + local files = fn.split(fn.globpath(folder, "*.md"), "\\n") + files = vim.tbl_filter(function(file) + return string.match(file, ZK_FILE_NAME_PATTERN) ~= nil + end, files) + + return files +end + +local function extract_id_and_title(line) + local zk_id = string.match(line, ZK_FULL_TITLE_PATTERN) + if zk_id == nil then + return nil + end + + zk_id = string.gsub(zk_id, "# ", "") + local note_id = string.match(zk_id, ZK_ID_PATTERN) + local title = vim.trim(string.gsub(zk_id, ZK_ID_PATTERN, "")) + + return { id = note_id, title = string.gsub(title, "\r", "") } +end + +local function extract_references(line, linenr) + assert(line ~= nil) + + local references = {} + for ref in string.gmatch(line, "(%[%[" .. ZK_ID_PATTERN .. "%]%])") do + ref = string.gsub(ref, "%[%[", "") + ref = string.gsub(ref, "%]%]", "") + table.insert(references, { id = ref, linenr = linenr }) + end + + return references +end + +local function extract_back_references(notes, note_id) + assert(note_id ~= nil) + + local back_references = {} + for _, note in ipairs(notes) do + if note.id == note_id then + goto continue + end + + local references = note.references + for _, ref in ipairs(references) do + if ref.id == note_id then + table.insert(back_references, { + id = note.id, + title = note.title, + file_name = note.file_name, + linenr = ref.linenr, + }) + + break + end + end + + ::continue:: + end + + return back_references +end + +local function extract_tags(line, linenr) + assert(line ~= nil) + + local tags = {} + for tag in string.gmatch(line, "(%#%a[%w-]+)") do + local start_pos, _ = string.find(line, tag, 1, true) + local previous_char = string.sub(line, start_pos - 1, start_pos - 1) + if previous_char == "" or previous_char == " " then + table.insert(tags, { linenr = linenr, name = tag }) + end + end + + return tags +end + +local function get_note_information(file_path) + local last_modified = fn.strftime("%Y-%m-%d.%H:%M:%S", fn.getftime(file_path)) + if + s_note_cache_with_file_path[file_path] ~= nil + and s_note_cache_with_file_path[file_path].last_modified == last_modified + then + return s_note_cache_with_file_path[file_path] + end + + local file = io.open(file_path, "r") + if file:read(0) == nil then + return nil + end + + local info = { + file_name = file_path, + last_modified = last_modified, + tags = {}, + references = {}, + back_references = {}, + } + + local linenr = 0 + while true do + linenr = linenr + 1 + local line = file:read("*line") + if line == nil then + break + end + + if line == "" then + goto continue + end + + if info.id == nil then + local id_title = extract_id_and_title(line) + if id_title then + info = vim.tbl_extend("error", info, id_title) + goto continue + end + end + + local refs = extract_references(line, linenr) + if refs then + vim.list_extend(info.references, refs) + end + + local tags = extract_tags(line, linenr) + if tags then + vim.list_extend(info.tags, tags) + end + + ::continue:: + end + + s_note_cache_with_file_path[file_path] = info + s_note_cache_with_id[info.id] = info + return info +end + +function M.get_note(id) + if s_note_cache_with_id[id] ~= nil then + return s_note_cache_with_id[id] + end + + local _ = M.get_notes() + + if s_note_cache_with_id[id] ~= nil then + return s_note_cache_with_id[id] + end + + return nil +end + +function M.get_notes() + local folder = config.get().notes_path + local files = get_files(folder) + local all_notes = {} + for _, file in ipairs(files) do + table.insert(all_notes, get_note_information(file)) + end + + for _, note in ipairs(all_notes) do + if note.id == nil then + goto continue + end + + local back_references = extract_back_references(all_notes, note.id) + if back_references then + -- When notes are cached, `back_references` field will have the references from before. + -- Since the files that refer to this one might have changed, we'll overwrite it here. + -- extract_back_references() already re-processes the references. + note.back_references = back_references + end + + ::continue:: + end + + return all_notes +end + +function M.get_tags() + local notes = M.get_notes() + local tags = {} + for _, note in ipairs(notes) do + if #note.tags == 0 then + goto continue + end + + for _, tag in ipairs(note.tags) do + table.insert(tags, { + linenr = tag.linenr, + name = tag.name, + file_name = note.file_name, + }) + end + + ::continue:: + end + + return tags +end + +return M diff --git a/bundle/vim-zettelkasten/lua/zettelkasten/config.lua b/bundle/vim-zettelkasten/lua/zettelkasten/config.lua new file mode 100644 index 000000000..62fe69a18 --- /dev/null +++ b/bundle/vim-zettelkasten/lua/zettelkasten/config.lua @@ -0,0 +1,31 @@ +--============================================================================= +-- config.lua --- the config module for zettelkasten +-- Copyright (c) 2016-2022 Wang Shidong & Contributors +-- Author: Wang Shidong < wsdjeg@outlook.com > +-- URL: https://spacevim.org +-- License: GPLv3 +--============================================================================= +local M = {} + + +if vim.g.zettelkasten_directory and vim.g.zettelkasten_directory ~= '' then + M.zettel_dir = vim.g.zettelkasten_directory +else + M.zettel_dir = '~/.zettelkasten/' +end + +local s_config = { + notes_path = "", + preview_command = "pedit", + browseformat = "%f - %h [%r Refs] [%b B-Refs] %t", +} + +M.get = function() + return s_config +end + +M._set = function(new_config) + s_config = vim.tbl_extend("force", s_config, new_config) +end + +return M diff --git a/bundle/vim-zettelkasten/lua/zettelkasten/formatter.lua b/bundle/vim-zettelkasten/lua/zettelkasten/formatter.lua new file mode 100644 index 000000000..9e7425869 --- /dev/null +++ b/bundle/vim-zettelkasten/lua/zettelkasten/formatter.lua @@ -0,0 +1,54 @@ +local M = {} +local s_formatters = { + ["%r"] = function(line) + return #line.references + end, + ["%b"] = function(line) + return #line.back_references + end, + ["%f"] = function(line) + return vim.fn.fnamemodify(line.file_name, ":t") + end, + ["%h"] = function(line) + return line.title + end, + ["%d"] = function(line) + return line.id + end, + ["%t"] = function(line) + local tags = {} + for _, tag in ipairs(line.tags) do + if vim.tbl_contains(tags, tag.name) == false then + table.insert(tags, tag.name) + end + end + + return table.concat(tags, " ") + end, +} + +local function get_format_keys(format) + local matches = {} + for w in string.gmatch(format, "%%%a") do + table.insert(matches, w) + end + + return matches +end + +function M.format(lines, format) + local formatted_lines = {} + local modifiers = get_format_keys(format) + for _, line in ipairs(lines) do + local cmps = format + for _, modifier in ipairs(modifiers) do + cmps = string.gsub(cmps, "%" .. modifier, s_formatters[modifier](line)) + end + + table.insert(formatted_lines, cmps) + end + + return formatted_lines +end + +return M diff --git a/bundle/vim-zettelkasten/lua/zettelkasten/log.lua b/bundle/vim-zettelkasten/lua/zettelkasten/log.lua new file mode 100644 index 000000000..ed21908d2 --- /dev/null +++ b/bundle/vim-zettelkasten/lua/zettelkasten/log.lua @@ -0,0 +1,23 @@ +local M = {} +local log_levels = vim.log.levels +local s_log_level = log_levels.INFO + +local logger = require("spacevim.logger").derive("zettel") + +function M.set_level(level) + s_log_level = level +end + +function M.notify(msg, level, opts) + if level >= s_log_level then + local tag = opts.tag or "[zettelkasten]" + vim.notify(tag .. " " .. msg, level, opts) + end +end + +function M.info(msg) -- {{{ + logger.info(msg) +end +-- }}} + +return M diff --git a/bundle/vim-zettelkasten/plugin/zettelkasten.lua b/bundle/vim-zettelkasten/plugin/zettelkasten.lua new file mode 100644 index 000000000..78ffc15f9 --- /dev/null +++ b/bundle/vim-zettelkasten/plugin/zettelkasten.lua @@ -0,0 +1,31 @@ +local api = vim.api +local s_config = require("zettelkasten.config") + +if vim.fn.exists(":ZkNew") == 0 then + vim.cmd([[command ZkNew :lua _G.zettelkasten.zknew()]]) +end + +if vim.fn.exists(":ZkBrowse") == 0 then + vim.cmd([[command ZkBrowse :lua _G.zettelkasten.zkbrowse()]]) +end + +_G.zettelkasten = { + tagfunc = require("zettelkasten").tagfunc, + completefunc = require("zettelkasten").completefunc, + zknew = function() + vim.cmd([[new | setlocal filetype=markdown]]) + if s_config.zettel_dir ~= "" then + if vim.fn.isdirectory(s_config.zettel_dir) == 0 then + vim.fn.mkdir(vim.fn.expand(s_config.zettel_dir), 'p', '0700') + end + vim.cmd("lcd " .. s_config.zettel_dir) + end + + vim.cmd("normal ggI# New Note") + require("zettelkasten").set_note_id(vim.api.nvim_get_current_buf()) + vim.cmd("normal $") + end, + zkbrowse = function() + vim.cmd("edit zk://browser") + end, +} diff --git a/bundle/vim-zettelkasten/stylua.toml b/bundle/vim-zettelkasten/stylua.toml new file mode 100644 index 000000000..a9418b8f2 --- /dev/null +++ b/bundle/vim-zettelkasten/stylua.toml @@ -0,0 +1,2 @@ +indent_type = "Spaces" +column_width = 100 diff --git a/bundle/vim-zettelkasten/syntax/zkbrowser.vim b/bundle/vim-zettelkasten/syntax/zkbrowser.vim new file mode 100644 index 000000000..3fb66113a --- /dev/null +++ b/bundle/vim-zettelkasten/syntax/zkbrowser.vim @@ -0,0 +1,13 @@ +if "zkbrowser" !=# get(b:, "current_syntax", "zkbrowser") + finish +endif + +syntax match ZkFileName '[0-9]\+-[0-9]\+-[0-9]\+-[0-9]\+-[0-9]\+-[0-9]\+\.md' +syntax match ZkRefCount '\[[0-9]\+ .*\]' +syntax match ZkTag '#\<\k\+\>' + +highlight default link ZkFileName Label +highlight default link ZkRefCount Link +highlight default link ZkTag Tag + +let b:current_syntax = "zkbrowser" diff --git a/docs/layers/zettelkasten.md b/docs/layers/zettelkasten.md new file mode 100644 index 000000000..b58b43ea6 --- /dev/null +++ b/docs/layers/zettelkasten.md @@ -0,0 +1,38 @@ +--- +title: "SpaceVim zettelkasten layer" +description: "This layers adds extensive support for zettelkasten" +--- + +# [Available Layers](../) >> zettelkasten + + + +- [Description](#description) +- [Install](#install) +- [Layer options](#layer-options) +- [Key bindings](#key-bindings) + + + +## Description + +This layer adds support for zettelkasten in neovim. + +## Install + +To use this configuration layer, update your custom configuration file with: + +```toml +[[layers]] + name = "zettelkasten" +``` + +## Layer options + +- `zettel_dir`: set the default zettelkasten directory + +## Key bindings + +| Key bindings | description | +| ------------ | --------------- | +| `SPC m z n` | create new note |