diff --git a/plugins/default.nix b/plugins/default.nix index 434a031c..d16147a3 100644 --- a/plugins/default.nix +++ b/plugins/default.nix @@ -159,7 +159,7 @@ ./utils/nvim-colorizer.nix ./utils/nvim-osc52.nix ./utils/nvim-ufo.nix - ./utils/obsidian.nix + ./utils/obsidian ./utils/oil.nix ./utils/ollama.nix ./utils/persistence.nix diff --git a/plugins/utils/obsidian.nix b/plugins/utils/obsidian.nix deleted file mode 100644 index 13b5a68f..00000000 --- a/plugins/utils/obsidian.nix +++ /dev/null @@ -1,617 +0,0 @@ -{ - lib, - helpers, - config, - pkgs, - ... -}: -with lib; let - cfg = config.plugins.obsidian; - - configOptions = { - logLevel = helpers.defaultNullOpts.mkLogLevel "info" '' - Set the log level for obsidian.nvim. - ''; - - notesSubdir = helpers.mkNullOrStr '' - If you keep notes in a specific subdirectory of your vault. - ''; - - templates = { - subdir = helpers.mkNullOrStr '' - The name of the directory where templates are stored. - - Example: "templates" - ''; - - dateFormat = helpers.mkNullOrStr '' - Which date format to use. - - Example: "%Y-%m-%d" - ''; - - timeFormat = helpers.mkNullOrStr '' - Which time format to use. - - Example: "%H:%M" - ''; - - substitutions = - helpers.defaultNullOpts.mkNullable - (with helpers.nixvimTypes; attrsOf (either str rawLua)) - "{}" - "A map for custom variables, the key should be the variable and the value a function."; - }; - - noteIdFunc = helpers.mkNullOrLuaFn '' - Customize how names/IDs for new notes are created. - - Example: - ```lua - function(title) - -- Create note IDs in a Zettelkasten format with a timestamp and a suffix. - -- In this case a note with the title 'My new note' will be given an ID that looks - -- like '1657296016-my-new-note', and therefore the file name '1657296016-my-new-note.md' - local suffix = "" - if title ~= nil then - -- If title is given, transform it into valid file name. - suffix = title:gsub(" ", "-"):gsub("[^A-Za-z0-9-]", ""):lower() - else - -- If title is nil, just add 4 random uppercase letters to the suffix. - for _ = 1, 4 do - suffix = suffix .. string.char(math.random(65, 90)) - end - end - return tostring(os.time()) .. "-" .. suffix - end - ``` - ''; - - followUrlFunc = helpers.mkNullOrLuaFn '' - By default when you use `:ObsidianFollowLink` on a link to an external URL it will be - ignored but you can customize this behavior here. - - Example: - ```lua - function(url) - -- Open the URL in the default web browser. - vim.fn.jobstart({"open", url}) -- Mac OS - -- vim.fn.jobstart({"xdg-open", url}) -- linux - end - ``` - ''; - - noteFrontmatterFunc = helpers.mkNullOrLuaFn '' - You can customize the frontmatter data. - - Example: - ```lua - function(note) - -- This is equivalent to the default frontmatter function. - local out = { id = note.id, aliases = note.aliases, tags = note.tags } - -- `note.metadata` contains any manually added fields in the frontmatter. - -- So here we just make sure those fields are kept in the frontmatter. - if note.metadata ~= nil and not vim.tbl_isempty(note.metadata) then - for k, v in pairs(note.metadata) do - out[k] = v - end - end - return out - end - ``` - ''; - - disableFrontmatter = helpers.mkNullOrStrLuaFnOr types.bool '' - Boolean or a function that takes a filename and returns a boolean. - `true` indicates that you don't want obsidian.nvim to manage frontmatter. - - Default: `false` - ''; - - backlinks = { - height = helpers.defaultNullOpts.mkUnsignedInt 10 '' - The default height of the backlinks pane. - ''; - - wrap = helpers.defaultNullOpts.mkBool true '' - Whether or not to wrap lines. - ''; - }; - - completion = { - nvimCmp = helpers.mkNullOrOption types.bool '' - Set to false to disable completion. - - Default: `true` if `cmp` is enabled. - ''; - - minChars = helpers.defaultNullOpts.mkUnsignedInt 2 '' - Trigger completion at this many chars. - ''; - - newNotesLocation = helpers.defaultNullOpts.mkEnumFirstDefault ["current_dir" "notes_subdir"] '' - Where to put new notes created from completion. - - Valid options are - - "current_dir" - put new notes in same directory as the current buffer. - - "notes_subdir" - put new notes in the default notes subdirectory. - ''; - - prependNoteId = helpers.defaultNullOpts.mkBool true '' - Whether to add the note ID during completion. - - E.g. "[[Foo" completes to "[[foo|Foo]]" assuming "foo" is the ID of the note. - Mutually exclusive with 'prependNotePath' and 'usePathOnly'. - ''; - - prependNotePath = helpers.defaultNullOpts.mkBool false '' - Whether to add the note path during completion. - - E.g. "[[Foo" completes to "[[notes/foo|Foo]]" assuming "notes/foo.md" is the path of the note. - Mutually exclusive with 'prependNoteId' and 'usePathOnly'. - ''; - - usePathOnly = helpers.defaultNullOpts.mkBool false '' - Whether to only use paths during completion. - - E.g. "[[Foo" completes to "[[notes/foo]]" assuming "notes/foo.md" is the path of the note. - Mutually exclusive with 'prependNoteId' and 'prependNotePath'. - ''; - }; - - mappings = - helpers.defaultNullOpts.mkNullable - ( - with types; - attrsOf (submodule { - options = { - action = mkOption { - type = helpers.nixvimTypes.strLua; - description = "The lua code for this keymap action."; - apply = helpers.mkRaw; - }; - opts = - helpers.keymaps.mapConfigOptions - // { - buffer = helpers.defaultNullOpts.mkBool false '' - If true, the mapping will be effective in the current buffer only. - ''; - }; - }; - }) - ) - '' - { - gf = { - action = "require('obsidian').util.gf_passthrough"; - opts = { - noremap = false; - expr = true; - buffer = true; - }; - }; - - "ch" = { - action = "require('obsidian').util.toggle_checkbox"; - opts.buffer = true; - }; - } - '' - '' - Configure key mappings. - ''; - - dailyNotes = { - folder = helpers.mkNullOrStr '' - Optional, if you keep daily notes in a separate directory. - ''; - - dateFormat = helpers.mkNullOrStr '' - Optional, if you want to change the date format for the ID of daily notes. - - Example: "%Y-%m-%d" - ''; - - aliasFormat = helpers.mkNullOrStr '' - Optional, if you want to change the date format of the default alias of daily notes. - - Example: "%B %-d, %Y" - ''; - - template = helpers.mkNullOrStr '' - Optional, if you want to automatically insert a template from your template directory like - 'daily.md'. - ''; - }; - - useAdvancedUri = helpers.mkNullOrOption types.bool '' - Set to true if you use the Obsidian Advanced URI plugin. - https://github.com/Vinzent03/obsidian-advanced-uri - ''; - - openAppForeground = helpers.defaultNullOpts.mkBool false '' - Set to true to force `:ObsidianOpen` to bring the app to the foreground. - ''; - - finder = helpers.mkNullOrStr '' - By default commands like `:ObsidianSearch` will attempt to use `telescope.nvim`, `fzf-lua`, - `fzf.vim`, or `mini.pick` (in that order), and use the first one they find. - You can set this option to tell `obsidian.nvim` to always use this finder. - ''; - - sortBy = helpers.defaultNullOpts.mkEnum ["path" "modified" "accessed" "created"] "modified" '' - Sort search results by "path", "modified", "accessed", or "created". - The recommend value is "modified" and `true` for `sortReversed`, which means, for example, - that `:ObsidianQuickSwitch` will show the notes sorted by latest modified time. - ''; - - sortReversed = helpers.defaultNullOpts.mkBool true '' - Whether search results should be reversed. - ''; - - openNotesIn = helpers.defaultNullOpts.mkEnumFirstDefault ["current" "vsplit" "hsplit"] '' - Determines how certain commands open notes. - - The valid options are: - - "current" (the default) - to always open in the current window - - "vsplit" - to open in a vertical split if there's not already a vertical split - - "hsplit" - to open in a horizontal split if there's not already a horizontal split - ''; - - ui = { - enable = helpers.defaultNullOpts.mkBool true '' - Set to false to disable all additional syntax features. - ''; - - updateDebounce = helpers.defaultNullOpts.mkUnsignedInt 200 '' - Update delay after a text change (in milliseconds). - ''; - - checkboxes = - helpers.defaultNullOpts.mkNullable - ( - with types; - attrsOf ( - submodule { - options = { - char = mkOption { - type = with helpers.nixvimTypes; maybeRaw str; - description = "The character to use for this checkbox."; - }; - - hlGroup = mkOption { - type = with helpers.nixvimTypes; maybeRaw str; - description = "The name of the highlight group to use for this checkbox."; - }; - }; - } - ) - ) - '' - { - " " = { - char = "󰄱"; - hlGroup = "ObsidianTodo"; - }; - "x" = { - char = ""; - hlGroup = "ObsidianDone"; - }; - ">" = { - char = ""; - hlGroup = "ObsidianRightArrow"; - }; - "~" = { - char = "󰰱"; - hlGroup = "ObsidianTilde"; - }; - } - '' - '' - Define how various check-boxes are displayed. - You can also add more custom ones... - - NOTE: the 'char' value has to be a single character, and the highlight groups are defined - in the `ui.hlGroups` option. - ''; - - externalLinkIcon = { - char = helpers.defaultNullOpts.mkStr "" '' - Which character to use for the external link icon. - ''; - - hlGroup = helpers.defaultNullOpts.mkStr "ObsidianExtLinkIcon" '' - The name of the highlight group to use for the external link icon. - ''; - }; - - referenceText = { - hlGroup = helpers.defaultNullOpts.mkStr "ObsidianRefText" '' - The name of the highlight group to use for reference text. - ''; - }; - - highlightText = { - hlGroup = helpers.defaultNullOpts.mkStr "ObsidianHighlightText" '' - The name of the highlight group to use for highlight text. - ''; - }; - - tags = { - hlGroup = helpers.defaultNullOpts.mkStr "ObsidianTag" '' - The name of the highlight group to use for tags. - ''; - }; - - hlGroups = - helpers.defaultNullOpts.mkNullable - (with helpers.nixvimTypes; attrsOf highlight) - '' - { - ObsidianTodo = { - bold = true; - fg = "#f78c6c"; - }; - ObsidianDone = { - bold = true; - fg = "#89ddff"; - }; - ObsidianRightArrow = { - bold = true; - fg = "#f78c6c"; - }; - ObsidianTilde = { - bold = true; - fg = "#ff5370"; - }; - ObsidianRefText = { - underline = true; - fg = "#c792ea"; - }; - ObsidianExtLinkIcon = { - fg = "#c792ea"; - }; - ObsidianTag = { - italic = true; - fg = "#89ddff"; - }; - ObsidianHighlightText = { - bg = "#75662e"; - }; - } - '' - "Highlight group definitions."; - }; - - attachments = { - imgFolder = helpers.defaultNullOpts.mkStr "assets/imgs" '' - The default folder to place images in via `:ObsidianPasteImg`. - - If this is a relative path it will be interpreted as relative to the vault root. - You can always override this per image by passing a full path to the command instead of just - a filename. - ''; - - imgTextFunc = - helpers.defaultNullOpts.mkLuaFn - '' - function(client, path) - ---@type string - local link_path - local vault_relative_path = client:vault_relative_path(path) - if vault_relative_path ~= nil then - -- Use relative path if the image is saved in the vault dir. - link_path = vault_relative_path - else - -- Otherwise use the absolute path. - link_path = tostring(path) - end - local display_name = vim.fs.basename(link_path) - return string.format("![%s](%s)", display_name, link_path) - end - '' - '' - A function that determines the text to insert in the note when pasting an image. - It takes two arguments, the `obsidian.Client` and a plenary `Path` to the image file. - - ```lua - @param client obsidian.Client - @param path Path the absolute path to the image file - @return string - ``` - ''; - }; - - yamlParser = helpers.defaultNullOpts.mkEnumFirstDefault ["native" "yq"] '' - Set the YAML parser to use. - - The valid options are: - - "native" - uses a pure Lua parser that's fast but potentially misses some edge cases. - - "yq" - uses the command-line tool yq (https://github.com/mikefarah/yq), which is more robust - but much slower and needs to be installed separately. - - In general you should be using the native parser unless you run into a bug with it, in which - case you can temporarily switch to the "yq" parser until the bug is fixed. - ''; - }; -in { - meta.maintainers = [maintainers.GaetanLepage]; - - options.plugins.obsidian = - helpers.neovim-plugin.extraOptionsOptions - // { - enable = mkEnableOption "obsidian.nvim"; - - package = helpers.mkPackageOption "obsidian.nvim" pkgs.vimPlugins.obsidian-nvim; - - dir = helpers.mkNullOrOption types.str '' - Alternatively to `workspaces` - and for backwards compatibility - you can set `dir` to a - single path instead of `workspaces`. - - For example: - ```nix - dir = "~/vaults/work"; - ``` - ''; - - workspaces = - helpers.defaultNullOpts.mkNullable - ( - with types; - listOf - (types.submodule { - options = { - name = mkOption { - type = with helpers.nixvimTypes; maybeRaw str; - description = "The name for this workspace"; - }; - - path = mkOption { - type = with helpers.nixvimTypes; maybeRaw str; - description = "The of the workspace."; - }; - - overrides = configOptions; - }; - }) - ) - "[]" - '' - A list of vault names and paths. - Each path should be the path to the vault root. - If you use the Obsidian app, the vault root is the parent directory of the `.obsidian` - folder. - You can also provide configuration overrides for each workspace through the `overrides` - field. - ''; - - detectCwd = helpers.defaultNullOpts.mkBool false '' - Set to true to use the current directory as a vault; otherwise the first workspace - is opened by default. - ''; - } - // configOptions; - - config = mkIf cfg.enable { - extraPlugins = [cfg.package]; - - assertions = [ - { - assertion = let - nvimCmpEnabled = isBool cfg.completion.nvimCmp && cfg.completion.nvimCmp; - in - nvimCmpEnabled -> config.plugins.cmp.enable; - message = '' - Nixvim (plugins.obsidian): You have enabled `completion.nvimCmp` but `plugins.cmp.enable` is `false`. - You need to enable `cmp` to use this setting. - ''; - } - ]; - - extraConfigLua = let - processConfigOptions = configOptions: - with configOptions; { - log_level = logLevel; - notes_subdir = notesSubdir; - templates = with templates; { - inherit subdir; - date_format = dateFormat; - time_format = timeFormat; - inherit substitutions; - }; - note_id_func = noteIdFunc; - follow_url_func = followUrlFunc; - note_formatter_func = noteFrontmatterFunc; - disable_frontmatter = disableFrontmatter; - backlinks = with backlinks; { - inherit - height - wrap - ; - }; - completion = with completion; { - nvim_cmp = nvimCmp; - min_chars = minChars; - new_notes_location = newNotesLocation; - prepend_note_id = prependNoteId; - prepend_note_path = prependNotePath; - use_path_only = usePathOnly; - }; - inherit mappings; - daily_notes = with dailyNotes; { - inherit folder; - date_format = dateFormat; - alias_format = aliasFormat; - inherit template; - }; - use_advanced_uri = useAdvancedUri; - open_app_foreground = openAppForeground; - inherit finder; - sort_by = sortBy; - sort_reversed = sortReversed; - open_notes_in = openNotesIn; - ui = with ui; { - inherit enable; - update_debounce = updateDebounce; - checkboxes = - helpers.ifNonNull' checkboxes - ( - mapAttrs - ( - _: checkbox: { - inherit (checkbox) char; - hl_group = checkbox.hlGroup; - } - ) - checkboxes - ); - external_link_icon = with externalLinkIcon; { - inherit char; - hl_group = hlGroup; - }; - reference_text = with referenceText; { - hl_group = hlGroup; - }; - highlight_text = with highlightText; { - hl_group = hlGroup; - }; - tags = with tags; { - hl_group = hlGroup; - }; - hl_groups = hlGroups; - }; - attachments = with attachments; { - img_folder = imgFolder; - img_text_func = imgTextFunc; - }; - yaml_parser = yamlParser; - }; - - setupOptions = with cfg; - { - inherit dir; - workspaces = - helpers.ifNonNull' workspaces - ( - map - ( - workspaceConfig: { - inherit - (workspaceConfig) - name - path - ; - overrides = processConfigOptions workspaceConfig.overrides; - } - ) - workspaces - ); - detect_cwd = detectCwd; - } - // (processConfigOptions cfg) - // cfg.extraOptions; - in '' - require('obsidian').setup(${helpers.toLuaObject setupOptions}) - ''; - }; -} diff --git a/plugins/utils/obsidian/default.nix b/plugins/utils/obsidian/default.nix new file mode 100644 index 00000000..cc66f481 --- /dev/null +++ b/plugins/utils/obsidian/default.nix @@ -0,0 +1,175 @@ +{ + lib, + helpers, + config, + pkgs, + ... +}: +with lib; + helpers.neovim-plugin.mkNeovimPlugin config { + name = "obsidian"; + originalName = "obsidian.nvim"; + defaultPackage = pkgs.vimPlugins.obsidian-nvim; + + maintainers = [maintainers.GaetanLepage]; + + ## DEPRECATIONS + # Introduced 2024-03-12 + # TODO: remove 2024-05-12 + deprecateExtraOptions = true; + optionsRenamedToSettings = [ + "dir" + "logLevel" + "notesSubdir" + ["templates" "subdir"] + ["templates" "dateFormat"] + ["templates" "timeFormat"] + ["templates" "substitutions"] + "noteIdFunc" + "followUrlFunc" + "noteFrontmatterFunc" + "disableFrontmatter" + ["completion" "nvimCmp"] + ["completion" "minChars"] + "mappings" + ["dailyNotes" "folder"] + ["dailyNotes" "dateFormat"] + ["dailyNotes" "aliasFormat"] + ["dailyNotes" "template"] + "useAdvancedUri" + "openAppForeground" + "sortBy" + "sortReversed" + "openNotesIn" + ["ui" "enable"] + ["ui" "updateDebounce"] + ["ui" "externalLinkIcon" "char"] + ["ui" "externalLinkIcon" "hlGroup"] + ["ui" "referenceText" "hlGroup"] + ["ui" "highlightText" "hlGroup"] + ["ui" "tags" "hlGroup"] + ["ui" "hlGroups"] + ["attachments" "imgFolder"] + ["attachments" "imgTextFunc"] + "yamlParser" + ]; + imports = let + basePluginPath = ["plugins" "obsidian"]; + in + [ + ( # We have to remove the option here because the user could set old-style camelCase options in each workspaces element. + mkRemovedOptionModule + (basePluginPath ++ ["workspaces"]) + "Please use `plugins.obsidian.settings.workspaces` instead." + ) + ( + mkRenamedOptionModule + (basePluginPath ++ ["finder"]) + (basePluginPath ++ ["settings" "picker" "name"]) + ) + ( + # https://github.com/epwalsh/obsidian.nvim/blob/656d9c2c64528839db8b2d9a091843b3c90155a2/CHANGELOG.md?plain=1#L184 + mkRenamedOptionModule + (basePluginPath ++ ["completion" "newNotesLocation"]) + (basePluginPath ++ ["settings" "new_notes_location"]) + ) + ( # We have to remove the option here because the user could set old-style camelCase options in each checkbox element. + mkRemovedOptionModule + (basePluginPath ++ ["ui" "checkboxes"]) + "Please use `plugins.obsidian.settings.ui.checkboxes` instead." + ) + ] + ++ (map ( + optionPath: + mkRemovedOptionModule + (basePluginPath ++ optionPath) + "This option was deprecated by upstream." + ) + [ + ["detectCwd"] + ["backlinks"] + ["completion" "prependNoteId"] + ["completion" "prependNotePath"] + ["completion" "usePathOnly"] + ]); + + settingsOptions = let + opts = import ./options.nix {inherit lib helpers;}; + in + { + dir = helpers.mkNullOrOption types.str '' + Alternatively to `workspaces` - and for backwards compatibility - you can set `dir` to a + single path instead of `workspaces`. + + For example: + ```nix + dir = "~/vaults/work"; + ``` + ''; + + workspaces = + helpers.defaultNullOpts.mkNullable + ( + with types; + listOf + (types.submodule { + options = { + name = mkOption { + type = with helpers.nixvimTypes; maybeRaw str; + description = "The name for this workspace"; + }; + + path = mkOption { + type = with helpers.nixvimTypes; maybeRaw str; + description = "The of the workspace."; + }; + + overrides = opts; + }; + }) + ) + "[]" + '' + A list of vault names and paths. + Each path should be the path to the vault root. + If you use the Obsidian app, the vault root is the parent directory of the `.obsidian` + folder. + You can also provide configuration overrides for each workspace through the `overrides` + field. + ''; + } + // opts; + + settingsExample = { + workspaces = [ + { + name = "work"; + path = "~/obsidian/work"; + } + { + name = "startup"; + path = "~/obsidian/startup"; + } + ]; + new_notes_location = "current_dir"; + completion = { + nvim_cmp = true; + min_chars = 2; + }; + }; + + extraConfig = cfg: { + assertions = [ + { + assertion = let + nvimCmpEnabled = isBool cfg.settings.completion.nvim_cmp && cfg.settings.completion.nvim_cmp; + in + nvimCmpEnabled -> config.plugins.nvim-cmp.enable; + message = '' + Nixvim (plugins.obsidian): You have enabled `completion.nvim_cmp` but `plugins.cmp.enable` is `false`. + You need to enable `nvim-cmp` to use this setting. + ''; + } + ]; + }; + } diff --git a/plugins/utils/obsidian/options.nix b/plugins/utils/obsidian/options.nix new file mode 100644 index 00000000..ef80fd46 --- /dev/null +++ b/plugins/utils/obsidian/options.nix @@ -0,0 +1,556 @@ +{ + lib, + helpers, +}: +with lib; { + # https://github.com/epwalsh/obsidian.nvim/blob/main/lua/obsidian/config.lua + + log_level = helpers.defaultNullOpts.mkLogLevel "info" '' + Set the log level for obsidian.nvim. + ''; + + notes_subdir = helpers.mkNullOrStr '' + If you keep notes in a specific subdirectory of your vault. + ''; + + templates = { + subdir = helpers.mkNullOrStr '' + The name of the directory where templates are stored. + + Example: "templates" + ''; + + date_format = helpers.mkNullOrStr '' + Which date format to use. + + Example: "%Y-%m-%d" + ''; + + time_format = helpers.mkNullOrStr '' + Which time format to use. + + Example: "%H:%M" + ''; + + substitutions = + helpers.defaultNullOpts.mkAttrsOf + (with helpers.nixvimTypes; either str rawLua) + "{}" + "A map for custom variables, the key should be the variable and the value a function."; + }; + + new_notes_location = helpers.defaultNullOpts.mkEnumFirstDefault ["current_dir" "notes_subdir"] '' + Where to put new notes created from completion. + + Valid options are + - "current_dir" - put new notes in same directory as the current buffer. + - "notes_subdir" - put new notes in the default notes subdirectory. + ''; + + note_id_func = helpers.mkNullOrLuaFn '' + Customize how names/IDs for new notes are created. + + Example: + ```lua + function(title) + -- Create note IDs in a Zettelkasten format with a timestamp and a suffix. + -- In this case a note with the title 'My new note' will be given an ID that looks + -- like '1657296016-my-new-note', and therefore the file name '1657296016-my-new-note.md' + local suffix = "" + if title ~= nil then + -- If title is given, transform it into valid file name. + suffix = title:gsub(" ", "-"):gsub("[^A-Za-z0-9-]", ""):lower() + else + -- If title is nil, just add 4 random uppercase letters to the suffix. + for _ = 1, 4 do + suffix = suffix .. string.char(math.random(65, 90)) + end + end + return tostring(os.time()) .. "-" .. suffix + end + ``` + ''; + + note_path_func = helpers.mkNullOrLuaFn '' + Customize how note file names are generated given the ID, target directory, and title. + + ```lua + ---@param spec { id: string, dir: obsidian.Path, title: string|? } + ---@return string|obsidian.Path The full path to the new note. + ``` + + Example: + ```lua + function(spec) + -- This is equivalent to the default behavior. + local path = spec.dir / tostring(spec.id) + return path:with_suffix(".md") + end + ``` + ''; + + wiki_link_func = + helpers.mkNullOrLuaFn + '' + Customize how wiki links are formatted. + + ```lua + ---@param opts {path: string, label: string, id: string|?} + ---@return string + ``` + + Example: + ```lua + function(opts) + if opts.id == nil then + return string.format("[[%s]]", opts.label) + elseif opts.label ~= opts.id then + return string.format("[[%s|%s]]", opts.id, opts.label) + else + return string.format("[[%s]]", opts.id) + end + end + ``` + + Default: See source + ''; + + markdown_link_func = helpers.mkNullOrLuaFn '' + Customize how markdown links are formatted. + + ```lua + ---@param opts {path: string, label: string, id: string|?} + ---@return string links are formatted. + ``` + + Example: + ```lua + function(opts) + return string.format("[%s](%s)", opts.label, opts.path) + end + ``` + + Default: See source + ''; + + preferred_link_style = helpers.defaultNullOpts.mkEnumFirstDefault ["wiki" "markdown"] '' + Either 'wiki' or 'markdown'. + ''; + + follow_url_func = helpers.mkNullOrLuaFn '' + By default when you use `:ObsidianFollowLink` on a link to an external URL it will be + ignored but you can customize this behavior here. + + Example: + ```lua + function(url) + -- Open the URL in the default web browser. + vim.fn.jobstart({"open", url}) -- Mac OS + -- vim.fn.jobstart({"xdg-open", url}) -- linux + end + ``` + ''; + + image_name_func = helpers.mkNullOrLuaFn '' + Customize the default name or prefix when pasting images via `:ObsidianPasteImg`. + + Example: + ```lua + function() + -- Prefix image names with timestamp. + return string.format("%s-", os.time()) + end + ``` + ''; + + note_frontmatter_func = helpers.mkNullOrLuaFn '' + You can customize the frontmatter data. + + Example: + ```lua + function(note) + -- Add the title of the note as an alias. + if note.title then + note:add_alias(note.title) + end + + local out = { id = note.id, aliases = note.aliases, tags = note.tags } + + -- `note.metadata` contains any manually added fields in the frontmatter. + -- So here we just make sure those fields are kept in the frontmatter. + if note.metadata ~= nil and not vim.tbl_isempty(note.metadata) then + for k, v in pairs(note.metadata) do + out[k] = v + end + end + + return out + end + ``` + ''; + + disable_frontmatter = helpers.mkNullOrStrLuaFnOr types.bool '' + Boolean or a function that takes a filename and returns a boolean. + `true` indicates that you don't want obsidian.nvim to manage frontmatter. + + Default: `false` + ''; + + completion = { + nvim_cmp = helpers.mkNullOrOption types.bool '' + Set to false to disable completion. + + Default: `true` if `nvim-cmp` is enabled (`plugins.cmp.enable`). + ''; + + min_chars = helpers.defaultNullOpts.mkUnsignedInt 2 '' + Trigger completion at this many chars. + ''; + }; + + mappings = + helpers.defaultNullOpts.mkNullable + ( + with types; + attrsOf (submodule { + options = { + action = mkOption { + type = helpers.nixvimTypes.strLua; + description = "The lua code for this keymap action."; + apply = helpers.mkRaw; + }; + opts = + helpers.keymaps.mapConfigOptions + // { + buffer = helpers.defaultNullOpts.mkBool false '' + If true, the mapping will be effective in the current buffer only. + ''; + }; + }; + }) + ) + '' + { + gf = { + action = "require('obsidian').util.gf_passthrough"; + opts = { + noremap = false; + expr = true; + buffer = true; + }; + }; + + "ch" = { + action = "require('obsidian').util.toggle_checkbox"; + opts.buffer = true; + }; + } + '' + '' + Configure key mappings. + ''; + + picker = { + name = helpers.mkNullOrOption (types.enum ["telescope.nvim" "fzf-lua" "mini.pick"]) '' + Set your preferred picker. + ''; + + note_mappings = + helpers.defaultNullOpts.mkAttrsOf types.str + '' + { + new = ""; + insert_link = ""; + } + '' + '' + Optional, configure note mappings for the picker. These are the defaults. + Not all pickers support all mappings. + ''; + + tag_mappings = + helpers.defaultNullOpts.mkAttrsOf types.str + '' + { + tag_note = ""; + insert_tag = ""; + } + '' + '' + Optional, configure tag mappings for the picker. These are the defaults. + Not all pickers support all mappings. + ''; + }; + + daily_notes = { + folder = helpers.mkNullOrStr '' + Optional, if you keep daily notes in a separate directory. + ''; + + date_format = helpers.mkNullOrStr '' + Optional, if you want to change the date format for the ID of daily notes. + + Example: "%Y-%m-%d" + ''; + + alias_format = helpers.mkNullOrStr '' + Optional, if you want to change the date format of the default alias of daily notes. + + Example: "%B %-d, %Y" + ''; + + template = helpers.mkNullOrStr '' + Optional, if you want to automatically insert a template from your template directory like + 'daily.md'. + ''; + }; + + use_advanced_uri = helpers.defaultNullOpts.mkBool false '' + Set to true to force ':ObsidianOpen' to bring the app to the foreground. + ''; + + open_app_foreground = helpers.defaultNullOpts.mkBool false '' + Set to true to force `:ObsidianOpen` to bring the app to the foreground. + ''; + + sort_by = helpers.defaultNullOpts.mkEnum ["path" "modified" "accessed" "created"] "modified" '' + Sort search results by "path", "modified", "accessed", or "created". + The recommend value is "modified" and `true` for `sortReversed`, which means, for example, + that `:ObsidianQuickSwitch` will show the notes sorted by latest modified time. + ''; + + sort_reversed = helpers.defaultNullOpts.mkBool true '' + Whether search results should be reversed. + ''; + + open_notes_in = helpers.defaultNullOpts.mkEnumFirstDefault ["current" "vsplit" "hsplit"] '' + Determines how certain commands open notes. + + The valid options are: + - "current" (the default) - to always open in the current window + - "vsplit" - to open in a vertical split if there's not already a vertical split + - "hsplit" - to open in a horizontal split if there's not already a horizontal split + ''; + + ui = { + enable = helpers.defaultNullOpts.mkBool true '' + Set to false to disable all additional syntax features. + ''; + + update_debounce = helpers.defaultNullOpts.mkUnsignedInt 200 '' + Update delay after a text change (in milliseconds). + ''; + + checkboxes = + helpers.defaultNullOpts.mkNullable + ( + with types; + attrsOf ( + submodule { + options = { + char = mkOption { + type = with helpers.nixvimTypes; maybeRaw str; + description = "The character to use for this checkbox."; + }; + + hl_group = mkOption { + type = with helpers.nixvimTypes; maybeRaw str; + description = "The name of the highlight group to use for this checkbox."; + }; + }; + } + ) + ) + '' + { + " " = { + char = "󰄱"; + hl_group = "ObsidianTodo"; + }; + "x" = { + char = ""; + hl_group = "ObsidianDone"; + }; + ">" = { + char = ""; + hl_group = "ObsidianRightArrow"; + }; + "~" = { + char = "󰰱"; + hl_group = "ObsidianTilde"; + }; + } + '' + '' + Define how various check-boxes are displayed. + You can also add more custom ones... + + NOTE: the 'char' value has to be a single character, and the highlight groups are defined + in the `ui.hl_groups` option. + ''; + + bullets = { + char = helpers.defaultNullOpts.mkStr "•" '' + Which character to use for the bullets. + ''; + + hl_group = helpers.defaultNullOpts.mkStr "ObsidianBullet" '' + The name of the highlight group to use for the bullets. + ''; + }; + + external_link_icon = { + char = helpers.defaultNullOpts.mkStr "" '' + Which character to use for the external link icon. + ''; + + hl_group = helpers.defaultNullOpts.mkStr "ObsidianExtLinkIcon" '' + The name of the highlight group to use for the external link icon. + ''; + }; + + reference_text = { + hl_group = helpers.defaultNullOpts.mkStr "ObsidianRefText" '' + The name of the highlight group to use for reference text. + ''; + }; + + highlight_text = { + hl_group = helpers.defaultNullOpts.mkStr "ObsidianHighlightText" '' + The name of the highlight group to use for highlight text. + ''; + }; + + tags = { + hl_group = helpers.defaultNullOpts.mkStr "ObsidianTag" '' + The name of the highlight group to use for tags. + ''; + }; + + hl_groups = + helpers.defaultNullOpts.mkNullable + (with helpers.nixvimTypes; attrsOf highlight) + '' + { + ObsidianTodo = { + bold = true; + fg = "#f78c6c"; + }; + ObsidianDone = { + bold = true; + fg = "#89ddff"; + }; + ObsidianRightArrow = { + bold = true; + fg = "#f78c6c"; + }; + ObsidianTilde = { + bold = true; + fg = "#ff5370"; + }; + ObsidianRefText = { + underline = true; + fg = "#c792ea"; + }; + ObsidianExtLinkIcon = { + fg = "#c792ea"; + }; + ObsidianTag = { + italic = true; + fg = "#89ddff"; + }; + ObsidianHighlightText = { + bg = "#75662e"; + }; + } + '' + "Highlight group definitions."; + }; + + attachments = { + img_folder = helpers.defaultNullOpts.mkStr "assets/imgs" '' + The default folder to place images in via `:ObsidianPasteImg`. + + If this is a relative path it will be interpreted as relative to the vault root. + You can always override this per image by passing a full path to the command instead of just + a filename. + ''; + + img_text_func = + helpers.defaultNullOpts.mkLuaFn + '' + function(client, path) + ---@type string + local link_path + local vault_relative_path = client:vault_relative_path(path) + if vault_relative_path ~= nil then + -- Use relative path if the image is saved in the vault dir. + link_path = vault_relative_path + else + -- Otherwise use the absolute path. + link_path = tostring(path) + end + local display_name = vim.fs.basename(link_path) + return string.format("![%s](%s)", display_name, link_path) + end + '' + '' + A function that determines the text to insert in the note when pasting an image. + It takes two arguments, the `obsidian.Client` and a plenary `Path` to the image file. + + ```lua + @param client obsidian.Client + @param path Path the absolute path to the image file + @return string + ``` + ''; + + confirm_img_paste = helpers.defaultNullOpts.mkBool true '' + Whether to prompt for confirmation when pasting an image. + ''; + }; + + callbacks = { + post_setup = helpers.mkNullOrLuaFn '' + `fun(client: obsidian.Client)` + + Runs right after the `obsidian.Client` is initialized. + ''; + + enter_note = helpers.mkNullOrLuaFn '' + `fun(client: obsidian.Client, note: obsidian.Note)` + + Runs when entering a note buffer. + ''; + + leave_note = helpers.mkNullOrLuaFn '' + `fun(client: obsidian.Client, note: obsidian.Note)` + + Runs when leaving a note buffer. + ''; + + pre_write_note = helpers.mkNullOrLuaFn '' + `fun(client: obsidian.Client, note: obsidian.Note)` + + Runs right before writing a note buffer. + ''; + + post_set_workspace = helpers.mkNullOrLuaFn '' + `fun(client: obsidian.Client, workspace: obsidian.Workspace)` + + Runs anytime the workspace is set/changed. + ''; + }; + + yaml_parser = helpers.defaultNullOpts.mkEnumFirstDefault ["native" "yq"] '' + Set the YAML parser to use. + + The valid options are: + - "native" - uses a pure Lua parser that's fast but potentially misses some edge cases. + - "yq" - uses the command-line tool yq (https://github.com/mikefarah/yq), which is more robust + but much slower and needs to be installed separately. + + In general you should be using the native parser unless you run into a bug with it, in which + case you can temporarily switch to the "yq" parser until the bug is fixed. + ''; +} diff --git a/tests/test-sources/plugins/utils/obsidian.nix b/tests/test-sources/plugins/utils/obsidian.nix index 636eca66..75a109bc 100644 --- a/tests/test-sources/plugins/utils/obsidian.nix +++ b/tests/test-sources/plugins/utils/obsidian.nix @@ -1,207 +1,289 @@ { + # Note: we can only use `~/` as a workspace as the workspace folder need to exist in the sandbox. + empty = { - # TODO fix the plugin tests - plugins.obsidian.enable = false; + plugins.obsidian = { + enable = true; + # At least one workspaces is needed for the plugin to work + settings.workspaces = [ + { + name = "foo"; + path = "~/"; + } + ]; + }; }; - example = { + simple-example = { + plugins = { + cmp.enable = true; + + obsidian = { + enable = true; + + settings = { + dir = null; + workspaces = [ + { + name = "work"; + path = "~"; + } + ]; + new_notes_location = "current_dir"; + completion = { + nvim_cmp = true; + min_chars = 2; + }; + }; + }; + }; + }; + + complete-example = { plugins = { cmp.enable = true; obsidian = { enable = false; - dir = null; - workspaces = [ - { - name = "personal"; - path = "~/vaults/personal"; - } - { - name = "work"; - path = "~/vaults/work"; - overrides = { - notesSubdir = "notes"; - }; - } - ]; - detectCwd = false; - - logLevel = "info"; - notesSubdir = "notes"; - templates = { - # We cannot set this as it doesn't exist in the testing environment - # subdir = "templates"; - dateFormat = "%Y-%m-%d"; - timeFormat = "%H:%M"; - substitutions = {}; - }; - noteIdFunc = '' - function(title) - -- Create note IDs in a Zettelkasten format with a timestamp and a suffix. - -- In this case a note with the title 'My new note' will be given an ID that looks - -- like '1657296016-my-new-note', and therefore the file name '1657296016-my-new-note.md' - local suffix = "" - if title ~= nil then - -- If title is given, transform it into valid file name. - suffix = title:gsub(" ", "-"):gsub("[^A-Za-z0-9-]", ""):lower() - else - -- If title is nil, just add 4 random uppercase letters to the suffix. - for _ = 1, 4 do - suffix = suffix .. string.char(math.random(65, 90)) - end - end - return tostring(os.time()) .. "-" .. suffix - end - ''; - followUrlFunc = '' - function(url) - -- Open the URL in the default web browser. - vim.fn.jobstart({"open", url}) -- Mac OS - -- vim.fn.jobstart({"xdg-open", url}) -- linux - end - ''; - noteFrontmatterFunc = '' - function(note) - -- This is equivalent to the default frontmatter function. - local out = { id = note.id, aliases = note.aliases, tags = note.tags } - -- `note.metadata` contains any manually added fields in the frontmatter. - -- So here we just make sure those fields are kept in the frontmatter. - if note.metadata ~= nil and not vim.tbl_isempty(note.metadata) then - for k, v in pairs(note.metadata) do - out[k] = v - end - end - return out - end - ''; - disableFrontmatter = false; - backlinks = { - height = 10; - wrap = true; - }; - completion = { - nvimCmp = true; - minChars = 2; - newNotesLocation = "current_dir"; - prependNoteId = true; - prependNotePath = false; - usePathOnly = false; - }; - mappings = { - gf = { - action = "require('obsidian').util.gf_passthrough"; - opts = { - noremap = false; - expr = true; - buffer = true; - }; + settings = { + dir = null; + workspaces = [ + { + name = "work"; + path = "~/"; + overrides = { + notes_subdir = "notes"; + }; + } + ]; + log_level = "info"; + notes_subdir = "notes"; + templates = { + # We cannot set this as it doesn't exist in the testing environment + # subdir = "templates"; + date_format = "%Y-%m-%d"; + time_format = "%H:%M"; + substitutions = {}; }; - - "ch" = { - action = "require('obsidian').util.toggle_checkbox"; - opts.buffer = true; - }; - }; - dailyNotes = { - folder = "notes"; - dateFormat = "%Y-%m-%d"; - aliasFormat = "%B %-d, %Y"; - template = "daily.md"; - }; - useAdvancedUri = false; - openAppForeground = false; - finder = "telescope.nvim"; - sortBy = "modified"; - sortReversed = true; - openNotesIn = "current"; - ui = { - enable = true; - updateDebounce = 200; - checkboxes = { - " " = { - char = "󰄱"; - hlGroup = "ObsidianTodo"; - }; - "x" = { - char = ""; - hlGroup = "ObsidianDone"; - }; - ">" = { - char = ""; - hlGroup = "ObsidianRightArrow"; - }; - "~" = { - char = "󰰱"; - hlGroup = "ObsidianTilde"; - }; - }; - externalLinkIcon = { - char = ""; - hlGroup = "ObsidianExtLinkIcon"; - }; - referenceText = { - hlGroup = "ObsidianRefText"; - }; - highlightText = { - hlGroup = "ObsidianHighlightText"; - }; - tags = { - hlGroup = "ObsidianTag"; - }; - hlGroups = { - ObsidianTodo = { - bold = true; - fg = "#f78c6c"; - }; - ObsidianDone = { - bold = true; - fg = "#89ddff"; - }; - ObsidianRightArrow = { - bold = true; - fg = "#f78c6c"; - }; - ObsidianTilde = { - bold = true; - fg = "#ff5370"; - }; - ObsidianRefText = { - underline = true; - fg = "#c792ea"; - }; - ObsidianExtLinkIcon = { - fg = "#c792ea"; - }; - ObsidianTag = { - italic = true; - fg = "#89ddff"; - }; - ObsidianHighlightText = { - bg = "#75662e"; - }; - }; - }; - attachments = { - imgFolder = "assets/imgs"; - imgTextFunc = '' - function(client, path) - ---@type string - local link_path - local vault_relative_path = client:vault_relative_path(path) - if vault_relative_path ~= nil then - -- Use relative path if the image is saved in the vault dir. - link_path = vault_relative_path + new_notes_location = "current_dir"; + note_id_func = '' + function(title) + -- Create note IDs in a Zettelkasten format with a timestamp and a suffix. + -- In this case a note with the title 'My new note' will be given an ID that looks + -- like '1657296016-my-new-note', and therefore the file name '1657296016-my-new-note.md' + local suffix = "" + if title ~= nil then + -- If title is given, transform it into valid file name. + suffix = title:gsub(" ", "-"):gsub("[^A-Za-z0-9-]", ""):lower() else - -- Otherwise use the absolute path. - link_path = tostring(path) + -- If title is nil, just add 4 random uppercase letters to the suffix. + for _ = 1, 4 do + suffix = suffix .. string.char(math.random(65, 90)) + end end - local display_name = vim.fs.basename(link_path) - return string.format("![%s](%s)", display_name, link_path) + return tostring(os.time()) .. "-" .. suffix end ''; + note_path_func = '' + function(spec) + -- This is equivalent to the default behavior. + local path = spec.dir / tostring(spec.id) + return path:with_suffix(".md") + end + ''; + wiki_link_func = '' + function(opts) + if opts.id == nil then + return string.format("[[%s]]", opts.label) + elseif opts.label ~= opts.id then + return string.format("[[%s|%s]]", opts.id, opts.label) + else + return string.format("[[%s]]", opts.id) + end + end + ''; + markdown_link_func = '' + function(opts) + return string.format("[%s](%s)", opts.label, opts.path) + end + ''; + preferred_link_style = "wiki"; + follow_url_func = '' + function(url) + -- Open the URL in the default web browser. + vim.fn.jobstart({"open", url}) -- Mac OS + -- vim.fn.jobstart({"xdg-open", url}) -- linux + end + ''; + image_name_func = '' + function() + -- Prefix image names with timestamp. + return string.format("%s-", os.time()) + end + ''; + note_frontmatter_func = '' + function(note) + -- Add the title of the note as an alias. + if note.title then + note:add_alias(note.title) + end + + local out = { id = note.id, aliases = note.aliases, tags = note.tags } + + -- `note.metadata` contains any manually added fields in the frontmatter. + -- So here we just make sure those fields are kept in the frontmatter. + if note.metadata ~= nil and not vim.tbl_isempty(note.metadata) then + for k, v in pairs(note.metadata) do + out[k] = v + end + end + + return out + end + ''; + disable_frontmatter = false; + completion = { + nvim_cmp = true; + min_chars = 2; + }; + mappings = { + gf = { + action = "require('obsidian').util.gf_passthrough"; + opts = { + noremap = false; + expr = true; + buffer = true; + }; + }; + + "ch" = { + action = "require('obsidian').util.toggle_checkbox"; + opts.buffer = true; + }; + }; + picker = { + name = "telescope.nvim"; + note_mappings = { + new = ""; + insert_link = ""; + }; + tag_mappings = { + tag_note = ""; + insert_tag = ""; + }; + }; + daily_notes = { + folder = "notes"; + date_format = "%Y-%m-%d"; + alias_format = "%B %-d, %Y"; + template = "daily.md"; + }; + use_advanced_uri = false; + open_app_foreground = false; + sort_by = "modified"; + sort_reversed = true; + open_notes_in = "current"; + ui = { + enable = true; + update_debounce = 200; + checkboxes = { + " " = { + char = "󰄱"; + hl_group = "ObsidianTodo"; + }; + "x" = { + char = ""; + hl_group = "ObsidianDone"; + }; + ">" = { + char = ""; + hl_group = "ObsidianRightArrow"; + }; + "~" = { + char = "󰰱"; + hl_group = "ObsidianTilde"; + }; + }; + bullets = { + char = "•"; + hl_group = "ObsidianBullet"; + }; + external_link_icon = { + char = ""; + hl_group = "ObsidianExtLinkIcon"; + }; + reference_text = { + hl_group = "ObsidianRefText"; + }; + highlight_text = { + hl_group = "ObsidianHighlightText"; + }; + tags = { + hl_group = "ObsidianTag"; + }; + hl_groups = { + ObsidianTodo = { + bold = true; + fg = "#f78c6c"; + }; + ObsidianDone = { + bold = true; + fg = "#89ddff"; + }; + ObsidianRightArrow = { + bold = true; + fg = "#f78c6c"; + }; + ObsidianTilde = { + bold = true; + fg = "#ff5370"; + }; + ObsidianRefText = { + underline = true; + fg = "#c792ea"; + }; + ObsidianExtLinkIcon = { + fg = "#c792ea"; + }; + ObsidianTag = { + italic = true; + fg = "#89ddff"; + }; + ObsidianHighlightText = { + bg = "#75662e"; + }; + }; + }; + attachments = { + img_folder = "assets/imgs"; + img_text_func = '' + function(client, path) + ---@type string + local link_path + local vault_relative_path = client:vault_relative_path(path) + if vault_relative_path ~= nil then + -- Use relative path if the image is saved in the vault dir. + link_path = vault_relative_path + else + -- Otherwise use the absolute path. + link_path = tostring(path) + end + local display_name = vim.fs.basename(link_path) + return string.format("![%s](%s)", display_name, link_path) + end + ''; + confirm_img_paste = true; + }; + callbacks = { + post_setup = "function(client) end"; + enter_note = "function(client, note) end"; + leave_note = "function(client, note) end"; + pre_write_note = "function(client, note) end"; + post_set_workspace = "function(client, workspace) end"; + }; + yaml_parser = "native"; }; - yamlParser = "native"; }; }; };