epwalsh / obsidian.nvim

Obsidian 🤝 Neovim
Apache License 2.0
3.94k stars 180 forks source link

`obsidian.Client.list_tags()` gives back an error sometimes #618

Closed BinaryFly closed 3 months ago

BinaryFly commented 3 months ago

🐛 Describe the bug

I tried to write some functionality for myself which gives me the opportunity to add tags via the command line to obsidian. This allows me to add multiple tags at a time with completion and I like this more than the Telescope solution with ObsidianTags. I use the function list_tags on my client here though and that returns the following error:

Error running post_setup callback: ...client.lua:1458: calling 'find_tags' on bad self (table expected, got nil)

This is how I use the list_tags method on the client in my post_setup callback in the obsidian config:

    callbacks = {
        -- Runs at the end of `require("obsidian").setup()`.
        ---@param client obsidian.Client
        post_setup = function(client)
            -- stores the tags of obsidian
            local tagstore = client:list_tags()

            -- set a command that will refresh the global tags for completion
            -- We do this manually to avoid having to refresh all the tags every time we run `ObsidianAddTags`
            -- This would impact performance since 'list_tags' takes a while to complete, especially in bigger workspaces
            vim.api.nvim_create_user_command(
                "ObsidianRefreshTags",
                function()
                    tagstore = client:list_tags()
                end,
                {
                    desc = "Refreshes the list of tags to be used by ObsidianAddTags"
                }
            )

            -- Creating a completion function for the tags
            local function complete_obsidian_tag(arg_lead, cmd_line, cursor_pos)
                local result = {}

                -- adding all tags that satisfy the first few characters to our completion list
                for _, tag in ipairs(tagstore) do
                    -- makes sure the tag starts with the found string
                    if vim.startswith(tag, arg_lead) then
                        table.insert(result, tag)
                    end
                end

                -- sorting the table for better readability
                table.sort(result)

                return result
            end

            -- set a command that will add a new tag to the current buffer
            vim.api.nvim_create_user_command(
                "ObsidianAddTags",
                function(opts)
                    if (#opts.fargs == 0) then
                        vim.notify("Please provide a tag!", vim.log.levels.WARN)
                        return
                    end

                    local current_note = require("obsidian").Note.from_buffer()
                    for _, value in ipairs(opts.fargs) do
                        current_note:add_tag(value)
                    end
                    current_note:save_to_buffer(current_note:frontmatter())
                end,
                {
                    nargs = "*",
                    complete = complete_obsidian_tag,
                    desc = "Adds tags seperated by whitespace to the currently opened note"
                }
            )
        end,
    },

I might also just use this method wrong, maybe I didn't understand how to use this the right way as well, but I feel like this has more to do with the asynchronous nature of find_tags because it does work sometimes in the fact that it doesn't throw an error and all the tags are loaded.

Config

in lazy.nvim config:

    {
        "epwalsh/obsidian.nvim",
        version = "*", -- recommended, use latest release instead of latest commit
        lazy = true,
        -- we only want to load obsidian.nvim for markdown files in our vault
        -- or when running certain commands
        ft = { "markdown" },
        cmd = { "ObsidianSearch", "ObsidianQuickSwitch" },
        keys = require("core.mappings").lazy.obsidian,
        dependencies = {
            -- Required.
            "nvim-lua/plenary.nvim",
            "nvim-telescope/telescope.nvim",
            "hrsh7th/cmp-nvim-lsp",
            "nvim-treesitter/nvim-treesitter"
        },
        opts = require("plugins.configs._obsidian")
    },

My _obsidian.lua file

-- hides stuff as concealchar, or as listchar.
-- set to 2 to hide listchar completely
-- set to 3 to hide both completely

-- autocmd for setting the conceallevel to 2 when entering the obsidian workspace
local obsidian_augroup = vim.api.nvim_create_augroup("obsidian", { clear = true })
vim.api.nvim_create_autocmd({ "BufReadPre", "BufEnter", "BufWinEnter" }, {
    pattern = { "*.md" },
    group = obsidian_augroup,
    callback = function() vim.opt.conceallevel = 2 end,
    desc = "Set conceallevel to 2 when entering a markdown file"
})

-- autocmd for setting the conceallevel to 0 when leaving the obsidian workspace
vim.api.nvim_create_autocmd({ "BufReadPost", "BufLeave", "BufWinLeave" }, {
    pattern = { "*.md" },
    group = obsidian_augroup,
    callback = function() vim.opt.conceallevel = 0 end,
    desc = "Reset conceallevel when exiting markdown file"
})

return {
    workspaces = {
        {
            name = "Obsidian Vault",
            path = vim.g.obsidian_vault_path,
        }
    },

    daily_notes = {
        folder = "Dailies",
    },

    completion = { -- Set to false to disable completion.
        nvim_cmp = true,
        -- Trigger completion at 2 chars.
        min_chars = 2,
    },

    mappings = {
        ["gf"] = {
            action = function()
                return require("obsidian").util.gf_passthrough()
            end,
            opts = { noremap = false, expr = true, buffer = true },
        },
        -- Toggle check-boxes.
        ["<leader>ch"] = {
            action = function()
                return require("obsidian").util.toggle_checkbox()
            end,
            opts = { buffer = true },
        },
    },

    new_notes_location = "current_dir",

    -- Optional, customize how note IDs are generated given an optional title.
    ---@param title string|?
    ---@return string
    note_id_func = function(title)
        -- put the title first and then the unique id to make it easier to search with telescope on filenames
        local prefix = ""
        if title ~= nil then
            -- If title is given, transform it into valid file name.
            prefix = 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
                prefix = prefix .. string.char(math.random(65, 90))
            end
        end
        return tostring(prefix .. "-" .. os.time())
    end,

    ---@param url string
    follow_url_func = function(url)
        vim.fn.jobstart({ vim.g.browser, url }) -- linux
    end,

    callbacks = {
        -- Runs at the end of `require("obsidian").setup()`.
        ---@param client obsidian.Client
        post_setup = function(client)
            -- stores the tags of obsidian
            local tagstore = client:list_tags()

            -- set a command that will refresh the global tags for completion
            -- We do this manually to avoid having to refresh all the tags every time we run `ObsidianAddTags`
            -- This would impact performance since 'list_tags' takes a while to complete, especially in bigger workspaces
            vim.api.nvim_create_user_command(
                "ObsidianRefreshTags",
                function()
                    tagstore = client:list_tags()
                end,
                {
                    desc = "Refreshes the list of tags to be used by ObsidianAddTags"
                }
            )

            -- Creating a completion function for the tags
            local function complete_obsidian_tag(arg_lead, cmd_line, cursor_pos)
                local result = {}

                -- adding all tags that satisfy the first few characters to our completion list
                for _, tag in ipairs(tagstore) do
                    -- makes sure the tag starts with the found string
                    if vim.startswith(tag, arg_lead) then
                        table.insert(result, tag)
                    end
                end

                -- sorting the table for better readability
                table.sort(result)

                return result
            end

            -- set a command that will add a new tag to the current buffer
            vim.api.nvim_create_user_command(
                "ObsidianAddTags",
                function(opts)
                    if (#opts.fargs == 0) then
                        vim.notify("Please provide a tag!", vim.log.levels.WARN)
                        return
                    end

                    local current_note = require("obsidian").Note.from_buffer()
                    for _, value in ipairs(opts.fargs) do
                        current_note:add_tag(value)
                    end
                    current_note:save_to_buffer(current_note:frontmatter())
                end,
                {
                    nargs = "*",
                    complete = complete_obsidian_tag,
                    desc = "Adds tags seperated by whitespace to the currently opened note"
                }
            )
        end,
    },

    attachments = {
        img_folder = "assets/imgs", -- This is the default
        ---@param client obsidian.Client
        ---@param path obsidian.Path the absolute path to the image file
        ---@return string
        img_text_func = function(client, path)
            local relative_to_buffer_path = client:vault_relative_path(path) or path
            return string.format("![%s](%s)", path.name, relative_to_buffer_path)
        end,
    },
}

And all my mappings that I use for obsidian:

    obsidian = {
        {
            "<leader>oo",
            function()
                vim.api.nvim_cmd({ cmd = "edit", args = { vim.g.obsidian_vault_path .. "/index.md" }}, {})
            end,
            desc = "Obsidian/zettelkasten root file"
        },
        { "<leader>os",  "<cmd>ObsidianSearch<CR>",      desc = "Open obsidian search menu" },
        { "<leader>of",  "<cmd>ObsidianQuickSwitch<CR>", desc = "Open obsidian quick switch menu" },
        { "<leader>ot",  "<cmd>ObsidianTags<CR>",        desc = "Open obsidian tag finder" },
        { "<leader>opi", "<Cmd>ObsidianPasteImg<CR>",    desc = "Paste an image in obsidian" },
        { "<leader>obl", "<Cmd>ObsidianBacklinks<CR>",   desc = "Open backlinks menu for current note" },
        {
            "<leader>ogp",
            function()
                local obsidian_status_ok, obsidian = pcall(require, "obsidian")
                if not obsidian_status_ok then
                    error("obsidian.nvim has to be loaded to execute this keymap")
                    return
                end

                local path_to_paste = obsidian.get_client():vault_relative_path(vim.fn.expand('%:p'))
                vim.api.nvim_put({ path_to_paste.filename }, "", true, true)
            end,
            desc = "Paste the path of the current note relative from the vault root in the buffer"
        },
        { "<leader>oat", ":ObsidianAddTags ",            desc = "Get a tag from user input and put it in the currently opened note" },
        { "<leader>ort", "<cmd>ObsidianRefreshTags<CR>", desc = "Refreshes all the tags used for completion in obsidian add tags" }
    },

Environment

NVIM v0.10.0-dev-2671+gdc110cba3
Build type: RelWithDebInfo
LuaJIT 2.1.1710088188
Run "nvim -V1 -v" for more info
Obsidian.nvim v3.7.13 (0e9bc3a8fcbc3259dbf747c53d796ed329822c2c)
Status:
  â?¢ buffer directory: nil
  â?¢ working directory: ***
Workspaces:
  âo" active workspace: Workspace(name='Obsidian Vault', path='***', root='***')
Dependencies:
  âo" plenary.nvim: a3e3bc82a3f95c5ed0d7201546d5d2c19b20d683
  âo" nvim-cmp: 5260e5e8ecadaf13e6b82cf867a909f54e15fd07
  âo" telescope.nvim: d90956833d7c27e73c621a61f20b29fdb7122709
Integrations:
  âo" picker: TelescopePicker()
  âo" completion: enabled (nvim-cmp) âo- refs, âo- tags, âo- new
    all sources:
      â?¢ path
      â?¢ nvim_lsp
      â?¢ luasnip
      â?¢ buffer
Tools:
  âo" rg: ripgrep 14.1.0
Environment:
  â?¢ operating system: Windows
Config:
  â?¢ notes_subdir: nil
BinaryFly commented 3 months ago

I feel like this is a specific issue with Windows, why I don't know yet, but I tried replicating this on my linux machine but wasn't able to reproduce this bug.

epwalsh commented 3 months ago

Hey @BinaryFly, could definitely be specified to Windows or the NVIM build, not sure. I'm assuming the error is happening inside the command function you defined? To make it more robust you could try getting the client instance at runtime by changing this line:

- tagstore = client:list_tags()
+ tagstore = require("obsidian").get_client():list_tags()
BinaryFly commented 3 months ago

I tried your suggestion but unfortunately it gives back the same error, as mentioned before I also tried running the standalone command in neovim: :lua require("obsidian").get_client():list_tags(), but this also doesn't work unfortunately.

epwalsh commented 3 months ago

Huh, I'm not sure what's going on. Could you post the full traceback when you get a chance?

BinaryFly commented 3 months ago

Yes of course, the complete traceback for lua require("obsidian").get_client():list_tags():


E5108: Error executing lua ...cal/nvim-data/lazy/obsidian.nvim/lua/obsidian/client.lua:1480: calling 'find_tags' on bad self (table expected, got nil)                                                                                                                        
stack traceback:
[C]: in function 'find_tags'
    ...cal/nvim-data/lazy/obsidian.nvim/lua/obsidian/client.lua:1480: in function 'list_tags'
    [string ":lua"]:1: in main chunk 

I also added some vim.print statements to the find_tags function to determine what arguments were passed

--- Find all tags starting with the given search term(s).
---
---@param term string|string[] The search term.
---@param opts { search: obsidian.SearchOpts|?, timeout: integer|? }|?
---
---@return obsidian.TagLocation[]
Client.find_tags = function(self, term, opts)
    vim.print("Self: Client")
    vim.print(self)
    vim.print("")
    vim.print("Term:")
    vim.print(term)
    vim.print("")
    vim.print("Opts:")
    vim.print(opts)
  opts = opts or {}
  return block_on(function(cb)
    return self:find_tags_async(term, cb, { search = opts.search })
  end, opts.timeout)
end

This prints the following "self"

    Self: Client
<1>{
  _default_opts = {
    attachments = {
      confirm_img_paste = true,
      img_folder = "4. Archive/assets/imgs",
      img_text_func = <function 1>
    },
    callbacks = <2>{
      post_setup = <function 2>
    },
    completion = {
      min_chars = 2,
      nvim_cmp = true
    },
    daily_notes = {
      date_format = "%Y-%m-%d",
      default_tags = <3>{ "daily-notes" },
      folder = "4. Archive/Dailies",
      template = "daily.md"
    },
    disable_frontmatter = false,
    follow_url_func = <function 3>,
    log_level = 2,
    mappings = <4>{
      ["<leader>ch"] = {
        action = <function 4>,
        opts = {
          buffer = true
        }
      },
      gf = {
        action = <function 5>,
        opts = {
          buffer = true,
          expr = true,
          noremap = false
        }
      }
    },
    markdown_link_func = <function 6>,
    new_notes_location = "current_dir",
    note_id_func = <function 7>,
    open_app_foreground = false,
    open_notes_in = "current",
    picker = {
      note_mappings = <5>{
        insert_link = "<C-l>",
        new = "<C-x>"
      },
      tag_mappings = <6>{
        insert_tag = "<C-l>",
        tag_note = "<C-x>"
      }
    },
    preferred_link_style = "wiki",
    search_max_lines = 1000,
    sort_by = "modified",
    sort_reversed = true,
    templates = {
      folder = "4. Archive/Templates",
      substitutions = <7>{}
    },
    ui = {
      block_ids = <8>{
        hl_group = "ObsidianBlockID"
      },
      bullets = <9>{
        char = "•",
        hl_group = "ObsidianBullet"
      },
      checkboxes = <10>{
        [" "] = {
          char = "󰄱",
          hl_group = "ObsidianTodo",
          order = 1
        },
        ["!"] = {
          char = "",
          hl_group = "ObsidianImportant",
          order = 3
        },
        [">"] = {
          char = "",
          hl_group = "ObsidianRightArrow",
          order = 4
        },
        x = {
          char = "",
          hl_group = "ObsidianDone",
          order = 5
        },
        ["~"] = {
          char = "󰰱",
          hl_group = "ObsidianTilde",
          order = 2
        }
      },
      enable = true,
      external_link_icon = <11>{
        char = "",
        hl_group = "ObsidianExtLinkIcon"
      },
      highlight_text = <12>{
        hl_group = "ObsidianHighlightText"
      },
      hl_groups = <13>{
        ObsidianBlockID = {
          fg = "#89ddff",
          italic = true
        },
        ObsidianBullet = {
          bold = true,
          fg = "#89ddff"
        },
        ObsidianDone = {
          bold = true,
          fg = "#89ddff"
        },
        ObsidianExtLinkIcon = {
          fg = "#c792ea"
        },
        ObsidianHighlightText = {
          bg = "#75662e"
        },
        ObsidianImportant = {
          bold = true,
          fg = "#d73128"
        },
        ObsidianRefText = {
          fg = "#c792ea",
          underline = true
        },
        ObsidianRightArrow = {
          bold = true,
          fg = "#f78c6c"
        },
        ObsidianTag = {
          fg = "#89ddff",
          italic = true
        },
        ObsidianTilde = {
          bold = true,
          fg = "#ff5370"
        },
        ObsidianTodo = {
          bold = true,
          fg = "#f78c6c"
        }
      },
      max_file_length = 5000,
      reference_text = <14>{
        hl_group = "ObsidianRefText"
      },
      tags = <15>{
        hl_group = "ObsidianTag"
      },
      update_debounce = 200
    },
    wiki_link_func = <function 8>,
    workspaces = <16>{ {
        name = "Obsidian Vault",
        path = "C:\\Users\\BinaryFly/Documents/Obsidian Vault"
      } }
  },
  _quiet = false,
  buf_dir = {
    filename = "C:/Users/BinaryFly/Documents/Obsidian Vault",
    <metatable> = <17>{
      __div = <function 9>,
      __eq = <function 10>,
      __index = <function 11>,
      __tostring = <function 12>
    }
  },
  callback_manager = {
    callbacks = <table 2>,
    client = <table 1>,
    <metatable> = <18>{
      __eq = <function 13>,
      __index = {
        as_tbl = <function 14>,
        enter_note = <function 15>,
        init = <function 16>,
        leave_note = <function 17>,
        mt = <table 18>,
        new = <function 18>,
        post_set_workspace = <function 19>,
        post_setup = <function 20>,
        pre_write_note = <function 21>
      }
    }
  },
  current_workspace = {
    name = "Obsidian Vault",
    path = {
      filename = "C:/Users/BinaryFly/Documents/Obsidian Vault",
      <metatable> = <table 17>
    },
    root = {
      filename = "C:/Users/BinaryFly/Documents/Obsidian Vault",
      <metatable> = <table 17>
    },
    <metatable> = <19>{
      __eq = <function 22>,
      __index = {
        _unlock = <function 23>,
        as_tbl = <function 24>,
        get_default_workspace = <function 25>,
        get_from_opts = <function 26>,
        get_workspace_for_cwd = <function 27>,
        get_workspace_for_dir = <function 28>,
        init = <function 29>,
        lock = <function 30>,
        mt = <table 19>,
        new = <function 31>,
        new_from_buf = <function 32>,
        new_from_cwd = <function 33>,
        new_from_spec = <function 34>
      },
      __tostring = <function 35>
    }
  },
  dir = {
    filename = "C:/Users/BinaryFly/Documents/Obsidian Vault",
    <metatable> = <table 17>
  },
  log = {
    _log_level = 2,
    debug = <function 36>,
    err = <function 37>,
    err_once = <function 38>,
    error = <function 37>,
    error_once = <function 37>,
    flush = <function 39>,
    info = <function 40>,
    lazy_err = <function 41>,
    lazy_error = <function 41>,
    lazy_info = <function 42>,
    lazy_log = <function 43>,
    lazy_warn = <function 44>,
    log = <function 45>,
    log_once = <function 46>,
    set_level = <function 47>,
    warn = <function 48>,
    warn_once = <function 49>
  },
  opts = {
    attachments = {
      confirm_img_paste = true,
      img_folder = "4. Archive/assets/imgs",
      img_text_func = <function 1>
    },
    callbacks = <table 2>,
    completion = {
      min_chars = 2,
      nvim_cmp = true
    },
    daily_notes = {
      date_format = "%Y-%m-%d",
      default_tags = <table 3>,
      folder = "4. Archive/Dailies",
      template = "daily.md"
    },
    disable_frontmatter = false,
    follow_url_func = <function 3>,
    log_level = 2,
    mappings = <table 4>,
    markdown_link_func = <function 6>,
    new_notes_location = "current_dir",
    note_id_func = <function 7>,
    open_app_foreground = false,
    open_notes_in = "current",
    picker = {
      note_mappings = <table 5>,
      tag_mappings = <table 6>
    },
    preferred_link_style = "wiki",
    search_max_lines = 1000,
    sort_by = "modified",
    sort_reversed = true,
    templates = {
      folder = "4. Archive/Templates",
      substitutions = <table 7>
    },
    ui = {
      block_ids = <table 8>,
      bullets = <table 9>,
      checkboxes = <table 10>,
      enable = true,
      external_link_icon = <table 11>,
      highlight_text = <table 12>,
      hl_groups = <table 13>,
      max_file_length = 5000,
      reference_text = <table 14>,
      tags = <table 15>,
      update_debounce = 200
    },
    wiki_link_func = <function 8>,
    workspaces = <table 16>
  },
  <metatable> = <20>{
    __eq = <function 50>,
    __index = {
      _daily = <function 51>,
      _prepare_search_opts = <function 52>,
      _search_iter_async = <function 53>,
      _search_opts_from_arg = <function 54>,
      apply_async = <function 55>,
      apply_async_raw = <function 56>,
      as_tbl = <function 57>,
      command = <function 58>,
      create_note = <function 59>,
      current_note = <function 60>,
      daily = <function 61>,
      daily_note_path = <function 62>,
      find_backlinks = <function 63>,
      find_backlinks_async = <function 64>,
      find_files = <function 65>,
      find_files_async = <function 66>,
      find_notes = <function 67>,
      find_notes_async = <function 68>,
      find_tags = <function 69>,
      find_tags_async = <function 70>,
      follow_link_async = <function 71>,
      format_link = <function 72>,
      init = <function 73>,
      list_tags = <function 74>,
      list_tags_async = <function 75>,
      mt = <table 20>,
      new = <function 76>,
      new_note = <function 77>,
      new_note_id = <function 78>,
      new_note_path = <function 79>,
      open_note = <function 80>,
      opts_for_workspace = <function 81>,
      parse_title_id_path = <function 82>,
      path_is_note = <function 83>,
      picker = <function 84>,
      resolve_link_async = <function 85>,
      resolve_note = <function 86>,
      resolve_note_async = <function 87>,
      resolve_note_async_with_picker_fallback = <function 88>,
      search_defaults = <function 89>,
      set_workspace = <function 90>,
      should_save_frontmatter = <function 91>,
      switch_workspace = <function 92>,
      templates_dir = <function 93>,
      today = <function 94>,
      tomorrow = <function 95>,
      update_frontmatter = <function 96>,
      update_ui = <function 97>,
      vault_name = <function 98>,
      vault_relative_path = <function 99>,
      vault_root = <function 100>,
      write_note = <function 101>,
      write_note_to_buffer = <function 102>,
      yesterday = <function 103>
    },
    __tostring = <function 104>
  }
}

and these are the other arguments passed to find_tags

Term:

Opts:
{}
epwalsh commented 3 months ago

Everything being printed seems fine :/ You're right this is probably an async bug on Windows. I'm not sure what to do about this other than wait for the next Neovim release and hope they've fixed it. Sorry I can't be more help. Let me know if you find any more info.

BinaryFly commented 3 months ago

Yeah I think so too unfortunately, well I'll close the issue for now then and wait for the next nvim release for Windows...