From 95b52bb3171942e95a2224bf85bd2ca8973190fa Mon Sep 17 00:00:00 2001 From: kostabekre Date: Sun, 27 Apr 2025 19:50:55 +0300 Subject: [PATCH 01/42] cache init commit --- lua/obsidian/cache.lua | 25 +++++++++++++++++++++++++ lua/obsidian/health.lua | 1 + lua/obsidian/search.lua | 2 ++ test/obsidian/cache_spec.lua | 6 ++++++ 4 files changed, 34 insertions(+) create mode 100644 lua/obsidian/cache.lua create mode 100644 test/obsidian/cache_spec.lua diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua new file mode 100644 index 00000000..d8dad753 --- /dev/null +++ b/lua/obsidian/cache.lua @@ -0,0 +1,25 @@ +local is_sqlite_available, sqlite = pcall(require, "sqlite") +local abc = require "obsidian.abc" +local Note = require "obsidian.note" + +---@class obsidian.Cache : obsidian.ABC +--- +---@field client obsidian.Client +local Cache = abc.new_class() + +--- Cache description +--- +---@param client obsidian.Client +Cache.new = function(client) + local self = Cache.init() + self.client = client + + return self +end + +Cache.index_vault = function(self) + local notes = self.client:find_notes("zettel", { search = { sort = false } }) + vim.print(vim.inspect(notes)) +end + +return Cache diff --git a/lua/obsidian/health.lua b/lua/obsidian/health.lua index e6bd07a8..2b19c126 100644 --- a/lua/obsidian/health.lua +++ b/lua/obsidian/health.lua @@ -85,6 +85,7 @@ function M.check() start "Dependencies" info(" ✓ rg: %s", util.get_external_dependency_info "rg" or "not found") has_plugin("plenary.nvim", false) + has_plugin("sqlite", true) end return M diff --git a/lua/obsidian/search.lua b/lua/obsidian/search.lua index 6b981c92..90ff04f1 100644 --- a/lua/obsidian/search.lua +++ b/lua/obsidian/search.lua @@ -337,6 +337,8 @@ SearchOpts.to_ripgrep_opts = function(self) opts[#opts + 1] = "--smart-case" end + opts[#opts + 1] = "--color=never" + if self.exclude ~= nil then assert(type(self.exclude) == "table") for path in iter(self.exclude) do diff --git a/test/obsidian/cache_spec.lua b/test/obsidian/cache_spec.lua new file mode 100644 index 00000000..990c44b2 --- /dev/null +++ b/test/obsidian/cache_spec.lua @@ -0,0 +1,6 @@ +local obsidian = require "obsidian" +local client = obsidian.setup { dir = "/home/frainx8/Documents/TestVault" } + +local cache = require("obsidian.cache").new(client) + +cache:index_vault(client) From 10dfbac8e89df4227ba1f1d890d87f7bd46d0dc0 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Sun, 4 May 2025 22:58:52 +0300 Subject: [PATCH 02/42] rejected using sqlite, added basic vault indexing, filewatch --- lua/obsidian/cache.lua | 62 ++++++++++++++- lua/obsidian/client.lua | 5 ++ lua/obsidian/commands/index_vault.lua | 7 ++ lua/obsidian/commands/init.lua | 3 + lua/obsidian/filewatch.lua | 109 ++++++++++++++++++++++++++ lua/obsidian/note.lua | 11 ++- lua/obsidian/pickers/_telescope.lua | 44 +++++++---- lua/obsidian/pickers/picker.lua | 3 + test/obsidian/cache_spec.lua | 1 - 9 files changed, 223 insertions(+), 22 deletions(-) create mode 100644 lua/obsidian/commands/index_vault.lua create mode 100644 lua/obsidian/filewatch.lua diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index d8dad753..d8818235 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -1,5 +1,5 @@ -local is_sqlite_available, sqlite = pcall(require, "sqlite") local abc = require "obsidian.abc" +local search = require "obsidian.search" local Note = require "obsidian.note" ---@class obsidian.Cache : obsidian.ABC @@ -14,12 +14,68 @@ Cache.new = function(client) local self = Cache.init() self.client = client + require("obsidian.filewatch").watch(client.dir.filename, { + on_event = function(filename, events) + vim.print(filename .. "changed!") + end, + }, { + watch_entry = true, + recursive = true, + }) + return self end +--- Reads all notes in the vaults and returns filename of each note relative to the vault and note's aliases. +---@param client obsidian.Client +local get_links_from_vault = function(client) + local interator = search.find(client.dir, "", nil) + + local founded_aliases = {} + + local notepath = interator() + + while notepath do + local note = Note.from_file(notepath, { read_only_frontmatter = true }) + + local relative_path = note.path.filename:gsub(client.dir.filename .. "/", "") + local note_cache = { relative_path, note.aliases } + + table.insert(founded_aliases, note_cache) + + notepath = interator() + end + + return founded_aliases +end + Cache.index_vault = function(self) - local notes = self.client:find_notes("zettel", { search = { sort = false } }) - vim.print(vim.inspect(notes)) + local ok, founded_links = pcall(get_links_from_vault, self.client) + + if not ok then + error "couldn't get links from vault" + end + + local file, err = io.open("./temp.json", "w") + + if file then + file:write(vim.fn.json_encode(founded_links)) + file:close() + else + error("couldn't write vault index to file: " .. err) + end +end + +Cache.get_links_from_cache = function(self) + local file, err = io.open("./temp.json", "r") + + if file then + local links_json = file:read() + file:close() + return vim.fn.json_decode(links_json) + else + error("couldn't read vault index to file: " .. err) + end end return Cache diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index 8e11adbd..e5f39685 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -62,6 +62,7 @@ end --- ---@field current_workspace obsidian.Workspace The current workspace. ---@field dir obsidian.Path The root of the vault for the current workspace. +---@field cache obsidian.Cache ---@field opts obsidian.config.ClientOpts The client config. ---@field buf_dir obsidian.Path|? The parent directory of the current buffer. ---@field callback_manager obsidian.CallbackManager @@ -97,6 +98,8 @@ Client.new = function(opts) self:set_workspace(workspace) + self.cache = require("obsidian.cache").new(self) + return self end @@ -484,6 +487,8 @@ Client.find_notes_async = function(self, term, callback, opts) return nil end + vim.print(path) + local ok, res = pcall(Note.from_file_async, path, opts.notes) if ok then diff --git a/lua/obsidian/commands/index_vault.lua b/lua/obsidian/commands/index_vault.lua new file mode 100644 index 00000000..0ebf5975 --- /dev/null +++ b/lua/obsidian/commands/index_vault.lua @@ -0,0 +1,7 @@ +local log = require "obsidian.log" + +---@param client obsidian.Client +---@param data CommandArgs +return function(client, data) + client.cache:index_vault() +end diff --git a/lua/obsidian/commands/init.lua b/lua/obsidian/commands/init.lua index 75b273cc..c2274c6c 100644 --- a/lua/obsidian/commands/init.lua +++ b/lua/obsidian/commands/init.lua @@ -16,6 +16,7 @@ local command_lookups = { ObsidianTemplate = "obsidian.commands.template", ObsidianNewFromTemplate = "obsidian.commands.new_from_template", ObsidianQuickSwitch = "obsidian.commands.quick_switch", + ObsidianIndexVault = "obsidian.commands.index_vault", ObsidianLinkNew = "obsidian.commands.link_new", ObsidianLink = "obsidian.commands.link", ObsidianLinks = "obsidian.commands.links", @@ -157,6 +158,8 @@ M.register("ObsidianNewFromTemplate", { opts = { nargs = "?", desc = "Create a n M.register("ObsidianQuickSwitch", { opts = { nargs = "?", desc = "Switch notes" } }) +M.register("ObsidianIndexVault", { opts = { nargs = "?", desc = "Index vault" } }) + M.register("ObsidianLinkNew", { opts = { nargs = "?", range = true, desc = "Link selected text to a new note" } }) M.register("ObsidianLink", { diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua new file mode 100644 index 00000000..8ec086f1 --- /dev/null +++ b/lua/obsidian/filewatch.lua @@ -0,0 +1,109 @@ +local uv = vim.loop + +local make_default_error_cb = function(path, runnable) + return function(error, _) + error("fwatch.watch(" .. path .. ", " .. runnable .. ")" .. "encountered an error: " .. error) + end +end + +-- Watch path and calls on_event(filename, events) or on_error(error) +-- +-- opts: +-- is_oneshot -> don't reattach after running, no matter the return value +local function watch_with_function(path, on_event, on_error, opts) + opts = opts or {} + + local handle = uv.new_fs_event() + + if not handle then + error "couldn't create event handler" + end + + -- these are just the default values + local flags = { + watch_entry = opts.watch_entry, -- true = when dir, watch dir inode, not dir content + stat = false, -- true = don't use inotify/kqueue but periodic check, not implemented + recursive = opts.recursive, -- true = watch dirs inside dirs + } + + local unwatch_cb = function() + uv.fs_event_stop(handle) + end + + local event_cb = function(err, filename, events) + vim.print("file " .. filename .. " is changed") + error "what the hell" + if err then + on_error(error, unwatch_cb) + else + on_event(filename, events, unwatch_cb) + end + if opts.is_oneshot then + unwatch_cb() + end + end + + path = path .. "/Base" + local success, err, err_name = uv.fs_event_start(handle, path, flags, event_cb) + + if not success then + error("couldn't create fs event! error - " .. err .. " err_name: " .. err_name) + end + + return handle +end + +-- Watch a path and run given string as an ex command +-- +-- Internally creates on_event and on_error handler and +-- delegates to watch_with_function. +local function watch_with_string(path, string, opts) + local on_event = function(_, _) + vim.schedule(function() + vim.cmd(string) + end) + end + local on_error = make_default_error_cb(path, string) + return watch_with_function(path, on_event, on_error, opts) +end + +-- Sniff parameters and call appropriate watch handler +local function do_watch(path, runnable, opts) + if type(runnable) == "string" then + return watch_with_string(path, runnable, opts) + elseif type(runnable) == "table" then + assert(runnable.on_event, "must provide on_event to watch") + assert(type(runnable.on_event) == "function", "on_event must be a function") + + -- no on_error provided, make default + if runnable.on_error == nil then + table.on_error = make_default_error_cb(path, "on_event_cb") + end + + return watch_with_function(path, runnable.on_event, runnable.on_error, opts) + else + error("Unknown runnable type given to watch," .. " must be string or {on_event = function, on_error = function}.") + end +end + +M = { + -- create watcher + watch = function(path, vim_command_or_callback_table, opts) + opts = opts or {} + opts.is_oneshot = false + return do_watch(path, vim_command_or_callback_table, opts) + end, + -- stop watcher + unwatch = function(handle) + return uv.fs_event_stop(handle) + end, + -- create watcher that auto stops + once = function(path, vim_command_or_callback_table, opts) + opts = opts or {} + opts.is_oneshot = true + + return do_watch(path, vim_command_or_callback_table, opts) + end, +} + +return M diff --git a/lua/obsidian/note.lua b/lua/obsidian/note.lua index 9b8d6b24..3f958220 100644 --- a/lua/obsidian/note.lua +++ b/lua/obsidian/note.lua @@ -255,10 +255,11 @@ Note.get_field = function(self, key) end ---@class obsidian.note.LoadOpts ----@field max_lines integer|? ----@field load_contents boolean|? ----@field collect_anchor_links boolean|? ----@field collect_blocks boolean|? +---@field max_lines integer|? Stops reading file if reached max lines. +---@field read_only_frontmatter boolean|? Stops reading file if reached end of frontmatter. +---@field load_contents boolean|? Save contents of the file if not frontmatter or block to obsidian.Note.contents. Still reads whole file if other options are true. +---@field collect_anchor_links boolean|? Save anchor_links of the file if not in frontmatter or block to obsidian.Note.anchor_links. Still reads whole file if other options are true. +---@field collect_blocks boolean|? Save code blocks to obsidian.Note.blocks. Still reads whole file if other options are true. --- Initialize a note from a file. --- @@ -488,6 +489,8 @@ Note.from_lines = function(lines, path, opts) if line_idx > max_lines or (title and not opts.load_contents and not opts.collect_anchor_links and not opts.collect_blocks) + or (opts.read_only_frontmatter and not has_frontmatter) + or (opts.read_only_frontmatter and has_frontmatter and not at_boundary) then break end diff --git a/lua/obsidian/pickers/_telescope.lua b/lua/obsidian/pickers/_telescope.lua index d1b48a88..8b0bae40 100644 --- a/lua/obsidian/pickers/_telescope.lua +++ b/lua/obsidian/pickers/_telescope.lua @@ -126,6 +126,10 @@ end ---@param opts obsidian.PickerFindOpts|? Options. TelescopePicker.find_files = function(self, opts) + local pickers = require "telescope.pickers" + local finders = require "telescope.finders" + local conf = require("telescope.config").values + opts = opts or {} local prompt_title = self:_build_prompt { @@ -134,20 +138,32 @@ TelescopePicker.find_files = function(self, opts) selection_mappings = opts.selection_mappings, } - telescope.find_files { - prompt_title = prompt_title, - cwd = opts.dir and tostring(opts.dir) or tostring(self.client.dir), - find_command = self:_build_find_cmd(), - attach_mappings = function(_, map) - attach_picker_mappings(map, { - entry_key = "path", - callback = opts.callback, - query_mappings = opts.query_mappings, - selection_mappings = opts.selection_mappings, - }) - return true - end, - } + pickers + .new(opts, { + prompt_title = prompt_title, + cwd = opts.dir, + finder = finders.new_table { + results = opts.notes, + entry_maker = function(entry) + return { + value = entry, + display = entry[1], + ordinal = entry[1], + } + end, + }, + sorter = conf.generic_sorter(opts), + attach_mappings = function(_, map) + attach_picker_mappings(map, { + entry_key = "path", + callback = opts.callback, + query_mappings = opts.query_mappings, + selection_mappings = opts.selection_mappings, + }) + return true + end, + }) + :find() end ---@param opts obsidian.PickerGrepOpts|? Options. diff --git a/lua/obsidian/pickers/picker.lua b/lua/obsidian/pickers/picker.lua index 0b4ab6b0..fbb299d0 100644 --- a/lua/obsidian/pickers/picker.lua +++ b/lua/obsidian/pickers/picker.lua @@ -10,6 +10,8 @@ local Note = require "obsidian.note" ---@field calling_bufnr integer local Picker = abc.new_class() +---@param client obsidian.Client +---@return obsidian.Picker Picker.new = function(client) local self = Picker.init() self.client = client @@ -147,6 +149,7 @@ Picker.find_notes = function(self, opts) end return self:find_files { + notes = self.client.cache:get_links_from_cache(), prompt_title = opts.prompt_title or "Notes", dir = self.client.dir, callback = opts.callback, diff --git a/test/obsidian/cache_spec.lua b/test/obsidian/cache_spec.lua index 990c44b2..9e8d8f2a 100644 --- a/test/obsidian/cache_spec.lua +++ b/test/obsidian/cache_spec.lua @@ -2,5 +2,4 @@ local obsidian = require "obsidian" local client = obsidian.setup { dir = "/home/frainx8/Documents/TestVault" } local cache = require("obsidian.cache").new(client) - cache:index_vault(client) From 7f7a3eb0afac2f093f7b9596cbf165408a6f1845 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Mon, 5 May 2025 21:59:13 +0300 Subject: [PATCH 03/42] A note can be searched by alias --- lua/obsidian/cache.lua | 16 ++++++++++++++-- lua/obsidian/filewatch.lua | 4 ++-- lua/obsidian/note.lua | 2 +- lua/obsidian/pickers/_telescope.lua | 29 +++++++++++++++++++++++++---- lua/obsidian/pickers/picker.lua | 1 - 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index d8818235..34ce8fef 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -16,7 +16,12 @@ Cache.new = function(client) require("obsidian.filewatch").watch(client.dir.filename, { on_event = function(filename, events) - vim.print(filename .. "changed!") + -- TODO get the full path without async + local full_path = client:resolve_note(filename) + vim.print(full_path) + + -- TODO update the cache for the updated file + -- TODO implement fast search of the note end, }, { watch_entry = true, @@ -35,15 +40,21 @@ local get_links_from_vault = function(client) local notepath = interator() + --TODO add updated progress + local note_amount = 0 while notepath do local note = Note.from_file(notepath, { read_only_frontmatter = true }) local relative_path = note.path.filename:gsub(client.dir.filename .. "/", "") - local note_cache = { relative_path, note.aliases } + -- TODO: add last update time to updated notes that were updated when neovim was offline + -- TODO add typing + local note_cache = { note.path.filename, note.aliases, relative_path } table.insert(founded_aliases, note_cache) notepath = interator() + + note_amount = note_amount + 1 end return founded_aliases @@ -67,6 +78,7 @@ Cache.index_vault = function(self) end Cache.get_links_from_cache = function(self) + -- TODO: allow change the save location and use hidden name local file, err = io.open("./temp.json", "r") if file then diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index 8ec086f1..d0ea214a 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -31,8 +31,6 @@ local function watch_with_function(path, on_event, on_error, opts) end local event_cb = function(err, filename, events) - vim.print("file " .. filename .. " is changed") - error "what the hell" if err then on_error(error, unwatch_cb) else @@ -44,6 +42,8 @@ local function watch_with_function(path, on_event, on_error, opts) end path = path .. "/Base" + + -- todo: subscribe to subfolders if on linux local success, err, err_name = uv.fs_event_start(handle, path, flags, event_cb) if not success then diff --git a/lua/obsidian/note.lua b/lua/obsidian/note.lua index 3f958220..1e8bdd1e 100644 --- a/lua/obsidian/note.lua +++ b/lua/obsidian/note.lua @@ -490,7 +490,7 @@ Note.from_lines = function(lines, path, opts) line_idx > max_lines or (title and not opts.load_contents and not opts.collect_anchor_links and not opts.collect_blocks) or (opts.read_only_frontmatter and not has_frontmatter) - or (opts.read_only_frontmatter and has_frontmatter and not at_boundary) + or (opts.read_only_frontmatter and has_frontmatter and not in_frontmatter) then break end diff --git a/lua/obsidian/pickers/_telescope.lua b/lua/obsidian/pickers/_telescope.lua index 8b0bae40..c4eb73f1 100644 --- a/lua/obsidian/pickers/_telescope.lua +++ b/lua/obsidian/pickers/_telescope.lua @@ -1,6 +1,7 @@ local telescope = require "telescope.builtin" local telescope_actions = require "telescope.actions" local actions_state = require "telescope.actions.state" +local compat = require "obsidian.compat" local Path = require "obsidian.path" local abc = require "obsidian.abc" @@ -128,6 +129,8 @@ end TelescopePicker.find_files = function(self, opts) local pickers = require "telescope.pickers" local finders = require "telescope.finders" + local actions = require "telescope.actions" + local action_state = require "telescope.actions.state" local conf = require("telescope.config").values opts = opts or {} @@ -143,17 +146,35 @@ TelescopePicker.find_files = function(self, opts) prompt_title = prompt_title, cwd = opts.dir, finder = finders.new_table { - results = opts.notes, + results = self.client.cache:get_links_from_cache(), entry_maker = function(entry) + local names = compat.flatten { entry[3], entry[2] } + local name_with_aliases = table.concat(names, "|") + return { value = entry, - display = entry[1], - ordinal = entry[1], + display = name_with_aliases, + ordinal = name_with_aliases, + filename = entry[1], } end, }, sorter = conf.generic_sorter(opts), - attach_mappings = function(_, map) + attach_mappings = function(prompt_bufnr, map) + actions.select_default:replace(function() + actions.close(prompt_bufnr) + + local selection = action_state.get_selected_entry() + + if not selection or not selection.filename then + return + end + + vim.schedule(function() + self.client:open_note(selection.filename) + end) + end) + attach_picker_mappings(map, { entry_key = "path", callback = opts.callback, diff --git a/lua/obsidian/pickers/picker.lua b/lua/obsidian/pickers/picker.lua index fbb299d0..ccf7bb13 100644 --- a/lua/obsidian/pickers/picker.lua +++ b/lua/obsidian/pickers/picker.lua @@ -149,7 +149,6 @@ Picker.find_notes = function(self, opts) end return self:find_files { - notes = self.client.cache:get_links_from_cache(), prompt_title = opts.prompt_title or "Notes", dir = self.client.dir, callback = opts.callback, From e334d68717a086ea8320a62b748deac15482bbe0 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Tue, 6 May 2025 22:36:56 +0300 Subject: [PATCH 04/42] Updated the filewatch --- lua/obsidian/cache.lua | 29 ++++++++++++++++++----------- lua/obsidian/filewatch.lua | 28 +++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index 34ce8fef..0d093e75 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -7,6 +7,17 @@ local Note = require "obsidian.note" ---@field client obsidian.Client local Cache = abc.new_class() +local save_links_to_cache = function(links) + local file, err = io.open("./temp.json", "w") + + if file then + file:write(vim.fn.json_encode(links)) + file:close() + else + error("couldn't write vault index to file: " .. err) + end +end + --- Cache description --- ---@param client obsidian.Client @@ -16,9 +27,12 @@ Cache.new = function(client) require("obsidian.filewatch").watch(client.dir.filename, { on_event = function(filename, events) - -- TODO get the full path without async - local full_path = client:resolve_note(filename) - vim.print(full_path) + local links = self:get_links_from_cache() + for _, v in ipairs(links) do + if v[1] == filename then + print("founded!" .. filename) + end + end -- TODO update the cache for the updated file -- TODO implement fast search of the note @@ -67,14 +81,7 @@ Cache.index_vault = function(self) error "couldn't get links from vault" end - local file, err = io.open("./temp.json", "w") - - if file then - file:write(vim.fn.json_encode(founded_links)) - file:close() - else - error("couldn't write vault index to file: " .. err) - end + save_links_to_cache(founded_links) end Cache.get_links_from_cache = function(self) diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index d0ea214a..57eddf89 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -21,9 +21,9 @@ local function watch_with_function(path, on_event, on_error, opts) -- these are just the default values local flags = { - watch_entry = opts.watch_entry, -- true = when dir, watch dir inode, not dir content + watch_entry = false, -- true = if you pass dir, watch the dir inode only, not the dir content stat = false, -- true = don't use inotify/kqueue but periodic check, not implemented - recursive = opts.recursive, -- true = watch dirs inside dirs + recursive = opts.recursive, -- true = watch dirs inside dirs. For now only works on Windows and MacOS } local unwatch_cb = function() @@ -34,7 +34,29 @@ local function watch_with_function(path, on_event, on_error, opts) if err then on_error(error, unwatch_cb) else - on_event(filename, events, unwatch_cb) + -- Sometimes the event returns a number + if tonumber(filename) then + return + end + -- + -- Sometimes the event returns the path with ~ + if filename:sub(#filename) == "~" then + return + end + + local folder_path = uv.fs_event_getpath(handle) + + -- TODO prevent from multiple triggering + uv.fs_stat(table.concat { folder_path, "/", filename }, function(err, stat) + if err then + error(err) + else + print "update time: " + print(vim.inspect(stat.mtime)) + + on_event(filename, events, unwatch_cb) + end + end) end if opts.is_oneshot then unwatch_cb() From c109e6de19b43b6624d527b7bb70ec9c412160ba Mon Sep 17 00:00:00 2001 From: kostabekre Date: Fri, 9 May 2025 13:45:06 +0300 Subject: [PATCH 05/42] working version --- lua/obsidian/cache.lua | 266 +++++++++++++++++++++---- lua/obsidian/client.lua | 2 - lua/obsidian/commands/init.lua | 2 +- lua/obsidian/commands/quick_switch.lua | 2 +- lua/obsidian/config.lua | 18 ++ lua/obsidian/filewatch.lua | 190 ++++++++++-------- lua/obsidian/pickers/_telescope.lua | 117 +++++++---- lua/obsidian/pickers/picker.lua | 4 +- 8 files changed, 429 insertions(+), 172 deletions(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index 0d093e75..4c9ae190 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -1,99 +1,281 @@ local abc = require "obsidian.abc" local search = require "obsidian.search" local Note = require "obsidian.note" +local log = require "obsidian.log" +local EventTypes = require("obsidian.filewatch").EventTypes +local uv = vim.uv +---This class allows you to find the notes in your vault more quickly. +---It scans your vault and saves the founded metadata to the default cache file ".cache.json" +---in the root of your vault or in the path you specified. +---For example, this allows to search for alises of your +---notes in much shorter time. ---@class obsidian.Cache : obsidian.ABC --- ---@field client obsidian.Client local Cache = abc.new_class() -local save_links_to_cache = function(links) - local file, err = io.open("./temp.json", "w") +---Contains some information from the metadata of your note plus additional info. +---@class obsidian.cache.CacheNote +--- +---@field absolute_path string The full path to the note. +---@field relative_path string The relative path to the root of the vault. +---@field aliases string[] The alises of the note founded in the frontmatter. +---@field last_updated number The last time the note was updated in seconds since epoch. + +---Converts the links to json and saves to the file at the given path. +---@param links obsidian.cache.CacheNote[] +---@param note_path string|obsidian.Note +local save_cache_notes_to_file = function(links, note_path) + local save_path + + if type(note_path) == "obsidian.Note" then + save_path = note_path.path.filename + else + save_path = note_path + end + + local file, err = io.open(save_path, "w") if file then file:write(vim.fn.json_encode(links)) file:close() else - error("couldn't write vault index to file: " .. err) + error(table.concat { "Couldn't write vault index to the file: ", save_path, ". Description: ", err }) end end ---- Cache description ---- ----@param client obsidian.Client -Cache.new = function(client) - local self = Cache.init() - self.client = client +---@param self obsidian.Cache +---@return fun (absolute_path: string, event_type: obsidian.filewatch.EventType, stat: uv.fs_stat.result) +local create_on_file_change_callback = function(self) + return function(filename, event_type, stat) + vim.schedule(function() + local founded = false + + local links = self:get_cache_notes_from_file() + for i, v in ipairs(links) do + if v.absolute_path == filename then + if event_type == EventTypes.deleted then + table.remove(links, i) + else + local note = Note.from_file(filename, { read_only_frontmatter = true }) + + local relative_path = note.path.filename:gsub(self.client.dir.filename .. "/", "") + + links[i] = { + absolute_path = filename, + aliases = note.aliases, + relative_path = relative_path, + last_updated = stat.mtime.sec, + } + end + + founded = true + break + end + end - require("obsidian.filewatch").watch(client.dir.filename, { - on_event = function(filename, events) - local links = self:get_links_from_cache() - for _, v in ipairs(links) do - if v[1] == filename then - print("founded!" .. filename) + if not founded then + -- Unknown file that was deleted is not in the cache, so we don't need to do anything. + if event_type == EventTypes.deleted then + return end + + local new_note = Note.from_file(filename, { read_only_frontmatter = true }) + + local relative_path = new_note.path.filename:gsub(self.client.dir.filename .. "/", "") + + local new_cache = { + absolute_path = filename, + aliases = new_note.aliases, + relative_path = relative_path, + last_updated = stat.mtime.sec, + } + + table.insert(links, new_cache) + end + + save_cache_notes_to_file(links, self.client.opts.cache.cache_path) + end) + end +end + +---Checks for note cache that were updated outside the vault +---@param self obsidian.Cache +local check_cache_notes_are_fresh = function(self) + local note_cache_list = self:get_cache_notes_from_file() + + local completed = 0 + local total = #note_cache_list + local updated = {} + local on_done = function() + completed = completed + 1 + + if completed == total then + vim.schedule(function() + save_cache_notes_to_file(updated, self.client.opts.cache.cache_path) + end) + end + end + + for _, note_cache in ipairs(note_cache_list) do + uv.fs_stat(note_cache.absolute_path, function(err, stat) + if err then + err("Couldn't get stat from the file " .. note_cache.relative_path .. " when performing reindex: " .. err) end - -- TODO update the cache for the updated file - -- TODO implement fast search of the note + local aliases + if note_cache.last_updated ~= stat.mtime.sec then + local note = Note.from_file(note_cache.absolute_path, { read_only_frontmatter = true }) + aliases = note.aliases + else + aliases = note_cache.aliases + end + + ---@type obsidian.cache.CacheNote + table.insert(updated, { + absolute_path = note_cache.absolute_path, + last_updated = stat.mtime.sec, + aliases = aliases, + relative_path = note_cache.relative_path, + }) + + on_done() + end) + end +end + +---Checks that file exits +---@param path string +---@param callback fun (result: boolean) +local check_file_exists = function(path, callback) + uv.fs_stat(path, function(err, _) + if not err then + callback(true) + else + callback(false) + end + end) +end + +---Watches the vault for changes. +---@param self obsidian.Cache +local enable_filewatch = function(self) + local handlers = require("obsidian.filewatch").watch(self.client.dir.filename, create_on_file_change_callback(self)) + + vim.api.nvim_create_autocmd({ "QuitPre", "ExitPre" }, { + callback = function() + for _, handle in ipairs(handlers) do + if handle then + handle:stop() + if not handle.is_closing then + handle:close() + end + end + end end, - }, { - watch_entry = true, - recursive = true, }) +end + +---@param self obsidian.Cache +local check_vault_cache = function(self) + check_file_exists(self.client.opts.cache.cache_path, function(exists) + if exists then + vim.schedule(function() + check_cache_notes_are_fresh(self) + end) + else + vim.schedule(function() + self:index_vault() + end) + end + end) +end + +---@param client obsidian.Client +Cache.new = function(client) + local self = Cache.init() + self.client = client + + if client.opts.cache.use_cache then + enable_filewatch(self) + + check_vault_cache(self) + end return self end ---- Reads all notes in the vaults and returns filename of each note relative to the vault and note's aliases. +--- Reads all notes in the vaults and returns the founded data. ---@param client obsidian.Client -local get_links_from_vault = function(client) +---@return obsidian.cache.CacheNote[] +local get_cache_notes_from_vault = function(client) local interator = search.find(client.dir, "", nil) - local founded_aliases = {} + ---@type obsidian.cache.CacheNote[] + local created_note_caches = {} local notepath = interator() - --TODO add updated progress - local note_amount = 0 + --TODO add indexing progress while notepath do local note = Note.from_file(notepath, { read_only_frontmatter = true }) - local relative_path = note.path.filename:gsub(client.dir.filename .. "/", "") - -- TODO: add last update time to updated notes that were updated when neovim was offline - -- TODO add typing - local note_cache = { note.path.filename, note.aliases, relative_path } + local absolute_path = note.path.filename + local relative_path = absolute_path:gsub(client.dir.filename .. "/", "") - table.insert(founded_aliases, note_cache) + local file_stat = uv.fs_stat(absolute_path) + local last_updated - notepath = interator() + if type(file_stat) ~= "table" then + log.err(table.concat { "couldn't get file stat from file ", absolute_path }) + last_updated = 0 + else + last_updated = file_stat.mtime.sec + end + + ---@type obsidian.cache.CacheNote + local note_cache = { + absolute_path = absolute_path, + aliases = note.aliases, + relative_path = relative_path, + last_updated = last_updated, + } - note_amount = note_amount + 1 + table.insert(created_note_caches, note_cache) + + notepath = interator() end - return founded_aliases + return created_note_caches end +--- Reads all notes in the vaults and saves them to the cache file. +---@param self obsidian.Cache Cache.index_vault = function(self) - local ok, founded_links = pcall(get_links_from_vault, self.client) - - if not ok then - error "couldn't get links from vault" + if not self.client.opts.cache.use_cache then + log.error "The cache is disabled. Cannot index vault." end - save_links_to_cache(founded_links) + local founded_links = get_cache_notes_from_vault(self.client) + + save_cache_notes_to_file(founded_links, self.client.opts.cache.cache_path) + + log.info "Vault was indexed succesfully." end -Cache.get_links_from_cache = function(self) - -- TODO: allow change the save location and use hidden name - local file, err = io.open("./temp.json", "r") +---Reads the cache file from client.opts.cache.cache_path and returns founded note cache. +---@param self obsidian.Cache +---@return obsidian.cache.CacheNote[] +Cache.get_cache_notes_from_file = function(self) + local file, err = io.open(self.client.opts.cache.cache_path, "r") if file then local links_json = file:read() file:close() return vim.fn.json_decode(links_json) else - error("couldn't read vault index to file: " .. err) + error("couldn't read vault index from file: " .. err) end end diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index 04e8ca28..d8bfe8b9 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -487,8 +487,6 @@ Client.find_notes_async = function(self, term, callback, opts) return nil end - vim.print(path) - local ok, res = pcall(Note.from_file_async, path, opts.notes) if ok then diff --git a/lua/obsidian/commands/init.lua b/lua/obsidian/commands/init.lua index 786353a7..1f2fbbce 100644 --- a/lua/obsidian/commands/init.lua +++ b/lua/obsidian/commands/init.lua @@ -226,7 +226,7 @@ M.register("new_from_template", { nargs = "*" }) M.register("quick_switch", { nargs = "?" }) -M.register("index_vault", { opts = { nargs = "?", desc = "Index vault" } }) +M.register("index_vault", { nargs = "?" }) M.register("link_new", { nargs = "?", range = true }) diff --git a/lua/obsidian/commands/quick_switch.lua b/lua/obsidian/commands/quick_switch.lua index 7d5422fc..edcc8b09 100644 --- a/lua/obsidian/commands/quick_switch.lua +++ b/lua/obsidian/commands/quick_switch.lua @@ -10,7 +10,7 @@ return function(client, data) return end - picker:find_notes() + picker:find_notes { use_cache = client.opts.cache.use_cache } else client:resolve_note_async_with_picker_fallback(data.args, function(note) client:open_note(note) diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 2b87f12b..00105dd6 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -34,6 +34,7 @@ local config = {} ---@field callbacks obsidian.config.CallbackConfig ---@field legacy_commands boolean ---@field statusline obsidian.config.StatuslineOpts +---@field cache obsidian.config.CacheOpts config.ClientOpts = {} --- Get defaults. @@ -68,6 +69,7 @@ config.ClientOpts.default = function() ui = config.UIOpts.default(), attachments = config.AttachmentsOpts.default(), callbacks = config.CallbackConfig.default(), + cache = config.CacheOpts.default(), legacy_commands = true, ---@class obsidian.config.StatuslineOpts ---@field format? string @@ -264,6 +266,8 @@ config.ClientOpts.normalize = function(opts, defaults) table.insert(opts.workspaces, 1, { path = opts.dir }) end + opts.cache = tbl_override(defaults.cache, opts.cache) + return opts end @@ -529,4 +533,18 @@ config.CallbackConfig.default = function() return {} end +---@class obsidian.config.CacheOpts +--- +---@field use_cache boolean|? Use cache when searching for notes +---@field cache_path string The file where the cache will be saved +config.CacheOpts = {} + +---@return obsidian.config.CacheOpts +config.CacheOpts.default = function() + return { + use_cache = false, + cache_path = "./.cache.json", + } +end + return config diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index 57eddf89..a7595aee 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -1,71 +1,102 @@ local uv = vim.loop +local util = require "obsidian.util" -local make_default_error_cb = function(path, runnable) - return function(error, _) - error("fwatch.watch(" .. path .. ", " .. runnable .. ")" .. "encountered an error: " .. error) +local M = {} + +---@class obsidian.filewatch.FileWatchOpts + +---Creates default callback if error occured in fs_event_start. +---@param path string The filepath where error occured. +---@return fun(error: string) Default function which accepts an error. +local make_default_error_cb = function(path) + return function(error) + error(table.concat { "obsidian.watch(", path, ")", "encountered an error: ", error }) end end --- Watch path and calls on_event(filename, events) or on_error(error) --- --- opts: --- is_oneshot -> don't reattach after running, no matter the return value -local function watch_with_function(path, on_event, on_error, opts) - opts = opts or {} +---@enum obsidian.filewatch.EventType +M.EventTypes = { + unknown = 0, + changed = 1, + renamed = 2, + deleted = 3, +} +--- Watch path and calls on_event(filename, event_type) or on_error(error) +---@param path string +---@param on_event fun (absolute_path: string, event_type: obsidian.filewatch.EventType, stat: uv.fs_stat.result|?) +---@param on_error fun (err: string) +---@param opts {recursive: boolean} +---@return uv.uv_fs_event_t +local function watch_path(path, on_event, on_error, opts) local handle = uv.new_fs_event() if not handle then error "couldn't create event handler" end - -- these are just the default values local flags = { watch_entry = false, -- true = if you pass dir, watch the dir inode only, not the dir content stat = false, -- true = don't use inotify/kqueue but periodic check, not implemented recursive = opts.recursive, -- true = watch dirs inside dirs. For now only works on Windows and MacOS } - local unwatch_cb = function() - uv.fs_event_stop(handle) - end + --- Minimal time in milliseconds to allow the event to fire. + local MIN_INTERVAL = 50 + local last_received_files = {} local event_cb = function(err, filename, events) if err then - on_error(error, unwatch_cb) - else - -- Sometimes the event returns a number - if tonumber(filename) then - return - end - -- - -- Sometimes the event returns the path with ~ - if filename:sub(#filename) == "~" then - return - end - - local folder_path = uv.fs_event_getpath(handle) + on_error(err) + return + end - -- TODO prevent from multiple triggering - uv.fs_stat(table.concat { folder_path, "/", filename }, function(err, stat) - if err then - error(err) + --TODO add description why it's needed, and move all cheks to a function + local now = uv.now() + local founded = false + for i, value in ipairs(last_received_files) do + if value[1] == filename then + founded = true + if now - value[2] < MIN_INTERVAL then + return else - print "update time: " - print(vim.inspect(stat.mtime)) - - on_event(filename, events, unwatch_cb) + last_received_files[i] = { filename, now } + break end - end) + end end - if opts.is_oneshot then - unwatch_cb() + + if not founded then + last_received_files[#last_received_files + 1] = { filename, now } end - end - path = path .. "/Base" + if filename:sub(#filename - 2, #filename) ~= ".md" then + return + end + + local folder_path = uv.fs_event_getpath(handle) + + local full_path = table.concat { folder_path, "/", filename } + + uv.fs_stat(full_path, function(stat_err, stat) + if stat_err then + on_event(full_path, M.EventTypes.deleted, nil) + return + end + + local event_type + if events.change then + event_type = M.EventTypes.changed + elseif events.rename then + event_type = M.EventTypes.renamed + else + event_type = M.EventTypes.unknown + end + + on_event(full_path, event_type, stat) + end) + end - -- todo: subscribe to subfolders if on linux local success, err, err_name = uv.fs_event_start(handle, path, flags, event_cb) if not success then @@ -75,57 +106,46 @@ local function watch_with_function(path, on_event, on_error, opts) return handle end --- Watch a path and run given string as an ex command --- --- Internally creates on_event and on_error handler and --- delegates to watch_with_function. -local function watch_with_string(path, string, opts) - local on_event = function(_, _) - vim.schedule(function() - vim.cmd(string) - end) +---Create a watch handler which uses callback function when a file is changed. +---If an error occured, called on_error function. +---TODO if a new folder will be created, it won't be tracked +---@param path string +---@param callback fun (absolute_path: string, event_type: obsidian.filewatch.EventType, stat: uv.fs_stat.result|?) +---@param on_error fun (err: string)|? +---@return uv.uv_fs_event_t[] +M.watch = function(path, callback, on_error) + if not path or path == "" then + error "Path cannot be empty." + end + + if not callback then + error "Callback cannot be empty!" end - local on_error = make_default_error_cb(path, string) - return watch_with_function(path, on_event, on_error, opts) -end --- Sniff parameters and call appropriate watch handler -local function do_watch(path, runnable, opts) - if type(runnable) == "string" then - return watch_with_string(path, runnable, opts) - elseif type(runnable) == "table" then - assert(runnable.on_event, "must provide on_event to watch") - assert(type(runnable.on_event) == "function", "on_event must be a function") - - -- no on_error provided, make default - if runnable.on_error == nil then - table.on_error = make_default_error_cb(path, "on_event_cb") + if on_error == nil then + on_error = make_default_error_cb(path) + end + + local sysname = util.get_os() + + if sysname == util.OSType.Linux then + local handle = io.popen("fd -t directory -a --base-directory " .. path) + if not handle then + error "Failed to execute command" end - return watch_with_function(path, runnable.on_event, runnable.on_error, opts) + local subdirs_handlers = {} + + for dir in handle:lines() do + table.insert(subdirs_handlers, watch_path(dir, callback, on_error, { recursive = false })) + end + + handle:close() + + return subdirs_handlers else - error("Unknown runnable type given to watch," .. " must be string or {on_event = function, on_error = function}.") + return { watch_path(path, callback, on_error, { recursive = true }) } end end -M = { - -- create watcher - watch = function(path, vim_command_or_callback_table, opts) - opts = opts or {} - opts.is_oneshot = false - return do_watch(path, vim_command_or_callback_table, opts) - end, - -- stop watcher - unwatch = function(handle) - return uv.fs_event_stop(handle) - end, - -- create watcher that auto stops - once = function(path, vim_command_or_callback_table, opts) - opts = opts or {} - opts.is_oneshot = true - - return do_watch(path, vim_command_or_callback_table, opts) - end, -} - return M diff --git a/lua/obsidian/pickers/_telescope.lua b/lua/obsidian/pickers/_telescope.lua index c4eb73f1..80fec486 100644 --- a/lua/obsidian/pickers/_telescope.lua +++ b/lua/obsidian/pickers/_telescope.lua @@ -1,7 +1,6 @@ local telescope = require "telescope.builtin" local telescope_actions = require "telescope.actions" local actions_state = require "telescope.actions.state" -local compat = require "obsidian.compat" local Path = require "obsidian.path" local abc = require "obsidian.abc" @@ -16,6 +15,13 @@ local TelescopePicker = abc.new_class({ end, }, Picker) +---@class obsidian.pickers.telescope_picker.CacheSelectedEntry +--- +---@field value obsidian.cache.CacheNote[] +---@field display string +---@field ordinal string +---@field absolute_path string + ---@param prompt_bufnr integer ---@param keep_open boolean|? ---@return table|? @@ -125,14 +131,71 @@ local function attach_picker_mappings(map, opts) end end ----@param opts obsidian.PickerFindOpts|? Options. -TelescopePicker.find_files = function(self, opts) +---Creates custom picker to search the notes using obsidian.cache.NoteCache +---@param self obsidian.pickers.TelescopePicker +---@param prompt_title string +---@param opts obsidian.PickerFindOpts +---@return Picker +local create_cache_picker = function(self, prompt_title, opts) local pickers = require "telescope.pickers" local finders = require "telescope.finders" + local config = require("telescope.config").values local actions = require "telescope.actions" - local action_state = require "telescope.actions.state" - local conf = require("telescope.config").values + local picker_opts = { + prompt_title = prompt_title, + attach_mappings = function(prompt_bufnr, map) + actions.select_default:replace(function() + local selection = get_entry(prompt_bufnr, false) + + if not selection or not selection.absolute_path then + return + end + + vim.schedule(function() + self.client:open_note(selection.absolute_path) + end) + end) + + attach_picker_mappings(map, { + entry_key = "absolute_path", + callback = opts.callback, + query_mappings = opts.query_mappings, + selection_mappings = opts.selection_mappings, + }) + return true + end, + } + + return pickers.new(picker_opts, { + cwd = opts.dir, + finder = finders.new_table { + results = self.client.cache:get_cache_notes_from_file(), + ---@param entry obsidian.cache.CacheNote + ---@return obsidian.pickers.telescope_picker.CacheSelectedEntry + entry_maker = function(entry) + local concated_aliases = table.concat(entry.aliases, "|") + local display_name + if concated_aliases and concated_aliases ~= "" then + display_name = table.concat({ entry.relative_path, concated_aliases }, "|") + else + display_name = entry.relative_path + end + + return { + value = entry, + display = display_name, + ordinal = display_name, + absolute_path = entry.absolute_path, + } + end, + }, + sorter = config.generic_sorter(), + }) +end + +---@param opts obsidian.PickerFindOpts|? Options. +TelescopePicker.find_files = function(self, opts) opts = opts or {} local prompt_title = self:_build_prompt { @@ -141,40 +204,14 @@ TelescopePicker.find_files = function(self, opts) selection_mappings = opts.selection_mappings, } - pickers - .new(opts, { + if opts.use_cache then + create_cache_picker(self, prompt_title, opts):find() + else + telescope.find_files { prompt_title = prompt_title, - cwd = opts.dir, - finder = finders.new_table { - results = self.client.cache:get_links_from_cache(), - entry_maker = function(entry) - local names = compat.flatten { entry[3], entry[2] } - local name_with_aliases = table.concat(names, "|") - - return { - value = entry, - display = name_with_aliases, - ordinal = name_with_aliases, - filename = entry[1], - } - end, - }, - sorter = conf.generic_sorter(opts), - attach_mappings = function(prompt_bufnr, map) - actions.select_default:replace(function() - actions.close(prompt_bufnr) - - local selection = action_state.get_selected_entry() - - if not selection or not selection.filename then - return - end - - vim.schedule(function() - self.client:open_note(selection.filename) - end) - end) - + cwd = opts.dir and tostring(opts.dir) or tostring(self.client.dir), + find_command = self:_build_find_cmd(), + attach_mappings = function(_, map) attach_picker_mappings(map, { entry_key = "path", callback = opts.callback, @@ -183,8 +220,8 @@ TelescopePicker.find_files = function(self, opts) }) return true end, - }) - :find() + } + end end ---@param opts obsidian.PickerGrepOpts|? Options. diff --git a/lua/obsidian/pickers/picker.lua b/lua/obsidian/pickers/picker.lua index 21bce8b9..a670d1fc 100644 --- a/lua/obsidian/pickers/picker.lua +++ b/lua/obsidian/pickers/picker.lua @@ -41,6 +41,7 @@ end ---@field no_default_mappings boolean|? ---@field query_mappings obsidian.PickerMappingTable|? ---@field selection_mappings obsidian.PickerMappingTable|? +---@field use_cache boolean|? --- Find files in a directory. --- @@ -130,7 +131,7 @@ end --- Find notes by filename. --- ----@param opts { prompt_title: string|?, callback: fun(path: string)|?, no_default_mappings: boolean|? }|? Options. +---@param opts { prompt_title: string|?, callback: fun(path: string)|?, no_default_mappings: boolean|?, use_cache :boolean|? }|? Options. --- --- Options: --- `prompt_title`: Title for the prompt window. @@ -155,6 +156,7 @@ Picker.find_notes = function(self, opts) no_default_mappings = opts.no_default_mappings, query_mappings = query_mappings, selection_mappings = selection_mappings, + use_cache = opts.use_cache, } end From aa48f82cdf0d80e28c5f54f8bbba21b0f8070e18 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Fri, 9 May 2025 14:26:10 +0300 Subject: [PATCH 06/42] last todo --- lua/obsidian/filewatch.lua | 50 ++++++++++++++++++++---------------- lua/obsidian/health.lua | 1 - test/obsidian/cache_spec.lua | 5 ---- 3 files changed, 28 insertions(+), 28 deletions(-) delete mode 100644 test/obsidian/cache_spec.lua diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index a7595aee..9d70f5b8 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -22,6 +22,33 @@ M.EventTypes = { deleted = 3, } +--- Minimal time in milliseconds to allow the event to fire. +local MIN_INTERVAL = 50 + +---Check if the event is not a duplicate or the received name is not `~` or a number. +---@param filename string +---@param last_received_files {[string]: number|?} +---@return boolean +local can_fire_callback = function(filename, last_received_files) + local now = uv.now() + + local last_callback_time = last_received_files[filename] + + if last_callback_time then + if now - last_callback_time < MIN_INTERVAL then + return false + end + end + + last_received_files[filename] = now + + if filename:sub(#filename - 2, #filename) ~= ".md" then + return false + end + + return true +end + --- Watch path and calls on_event(filename, event_type) or on_error(error) ---@param path string ---@param on_event fun (absolute_path: string, event_type: obsidian.filewatch.EventType, stat: uv.fs_stat.result|?) @@ -41,8 +68,6 @@ local function watch_path(path, on_event, on_error, opts) recursive = opts.recursive, -- true = watch dirs inside dirs. For now only works on Windows and MacOS } - --- Minimal time in milliseconds to allow the event to fire. - local MIN_INTERVAL = 50 local last_received_files = {} local event_cb = function(err, filename, events) @@ -51,26 +76,7 @@ local function watch_path(path, on_event, on_error, opts) return end - --TODO add description why it's needed, and move all cheks to a function - local now = uv.now() - local founded = false - for i, value in ipairs(last_received_files) do - if value[1] == filename then - founded = true - if now - value[2] < MIN_INTERVAL then - return - else - last_received_files[i] = { filename, now } - break - end - end - end - - if not founded then - last_received_files[#last_received_files + 1] = { filename, now } - end - - if filename:sub(#filename - 2, #filename) ~= ".md" then + if not can_fire_callback(filename, last_received_files) then return end diff --git a/lua/obsidian/health.lua b/lua/obsidian/health.lua index 2b19c126..e6bd07a8 100644 --- a/lua/obsidian/health.lua +++ b/lua/obsidian/health.lua @@ -85,7 +85,6 @@ function M.check() start "Dependencies" info(" ✓ rg: %s", util.get_external_dependency_info "rg" or "not found") has_plugin("plenary.nvim", false) - has_plugin("sqlite", true) end return M diff --git a/test/obsidian/cache_spec.lua b/test/obsidian/cache_spec.lua deleted file mode 100644 index 9e8d8f2a..00000000 --- a/test/obsidian/cache_spec.lua +++ /dev/null @@ -1,5 +0,0 @@ -local obsidian = require "obsidian" -local client = obsidian.setup { dir = "/home/frainx8/Documents/TestVault" } - -local cache = require("obsidian.cache").new(client) -cache:index_vault(client) From dda1188d1a44c3030d47194a55b1c5e2cea65744 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Fri, 9 May 2025 15:45:19 +0300 Subject: [PATCH 07/42] updated read me, fixed test --- README.md | 13 ++++++++++++- doc/obsidian_api.txt | 10 ++++++---- lua/obsidian/commands/index_vault.lua | 2 -- lua/obsidian/search.lua | 2 -- test/obsidian/client_spec.lua | 2 +- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c80933c0..b3e84957 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,8 @@ These default keymaps will only be set if you are in a valid workspace and a mar - `:Obsidian yesterday` to open/create the daily note for the previous working day. +- `:Obsidian index_vault` to manually updated the cache of the vault. (see cache) + ### Demo [![2024-01-31 14 22 52](https://github.com/epwalsh/obsidian.nvim/assets/8812459/2986e1d2-13e8-40e2-9c9e-75691a3b662e)](https://github.com/epwalsh/obsidian.nvim/assets/8812459/2986e1d2-13e8-40e2-9c9e-75691a3b662e) @@ -125,7 +127,7 @@ These default keymaps will only be set if you are in a valid workspace and a mar - Neovim >= 0.10.0 - For completion and search features: - - Backend: [ripgrep](https://github.com/BurntSushi/ripgrep), see [ripgrep#installation](https://github.com/BurntSushi/ripgrep) + - Backend: [ripgrep](https://github.com/BurntSushi/ripgrep), see [ripgrep#installation](https://github.com/BurntSushi/ripgrep). - Frontend: a picker, see [Plugin dependencies](#plugin-dependencies) - Additional system dependencies: @@ -134,6 +136,9 @@ These default keymaps will only be set if you are in a valid workspace and a mar - **MacOS** users need [`pngpaste`](https://github.com/jcsalterego/pngpaste) (`brew install pngpaste`) for `:Obsidian paste_img`. - **Linux** users need xclip (X11) or wl-clipboard (Wayland) for `:Obsidian paste_img`. +- Optional dependencies: + - Backend: [fd](https://github.com/sharkdp/fd), see [fd#installation](https://github.com/sharkdp/fd#installation) for `:Obsidian index_vault` + ### Plugin dependencies The only **required** plugin dependency is [plenary.nvim](https://github.com/nvim-lua/plenary.nvim), but there are a number of optional dependencies that enhance the obsidian.nvim experience. @@ -603,6 +608,12 @@ require("obsidian").setup { enabled = true, format = "{{properties}} properties {{backlinks}} backlinks {{words}} words {{chars}} chars", }, + + -- Experimental feature, disabled by default. + cache = { + use_cache = false, + cache_path = "./.cache.json", + } } ``` diff --git a/doc/obsidian_api.txt b/doc/obsidian_api.txt index 3c2c323c..206fc1c3 100644 --- a/doc/obsidian_api.txt +++ b/doc/obsidian_api.txt @@ -54,6 +54,7 @@ Class ~ Fields ~ {current_workspace} `(obsidian.Workspace)` The current workspace. {dir} `(obsidian.Path)` The root of the vault for the current workspace. +{cache} `(obsidian.Cache)` {opts} `(obsidian.config.ClientOpts)` The client config. {buf_dir} `(obsidian.Path|? The)` parent directory of the current buffer. {callback_manager} `(obsidian.CallbackManager)` @@ -834,10 +835,11 @@ Return ~ Class ~ {obsidian.note.LoadOpts} Fields ~ -{max_lines} `(integer|)`? -{load_contents} `(boolean|)`? -{collect_anchor_links} `(boolean|)`? -{collect_blocks} `(boolean|)`? +{max_lines} `(integer|? Stops)` reading file if reached max lines. +{read_only_frontmatter} `(boolean|? Stops)` reading file if reached end of frontmatter. +{load_contents} `(boolean|? Save)` contents of the file if not frontmatter or block to obsidian.Note.contents. Still reads whole file if other options are true. +{collect_anchor_links} `(boolean|? Save)` anchor_links of the file if not in frontmatter or block to obsidian.Note.anchor_links. Still reads whole file if other options are true. +{collect_blocks} `(boolean|? Save)` code blocks to obsidian.Note.blocks. Still reads whole file if other options are true. ------------------------------------------------------------------------------ *obsidian.Note.from_file()* diff --git a/lua/obsidian/commands/index_vault.lua b/lua/obsidian/commands/index_vault.lua index 0ebf5975..b4ccfc3a 100644 --- a/lua/obsidian/commands/index_vault.lua +++ b/lua/obsidian/commands/index_vault.lua @@ -1,5 +1,3 @@ -local log = require "obsidian.log" - ---@param client obsidian.Client ---@param data CommandArgs return function(client, data) diff --git a/lua/obsidian/search.lua b/lua/obsidian/search.lua index a19af280..d50b86fc 100644 --- a/lua/obsidian/search.lua +++ b/lua/obsidian/search.lua @@ -337,8 +337,6 @@ SearchOpts.to_ripgrep_opts = function(self) opts[#opts + 1] = "--smart-case" end - opts[#opts + 1] = "--color=never" - if self.exclude ~= nil then assert(type(self.exclude) == "table") for path in iter(self.exclude) do diff --git a/test/obsidian/client_spec.lua b/test/obsidian/client_spec.lua index a2bc1d1f..4f7f7e9c 100644 --- a/test/obsidian/client_spec.lua +++ b/test/obsidian/client_spec.lua @@ -160,7 +160,7 @@ describe("Client:parse_title_id_path()", function() with_tmp_client(function(client) client.opts.note_path_func = function(_) return "foo-bar-123.md" - end; + end (client.dir / "notes"):mkdir { exist_ok = true } From 3037db72702a43ca5d6799e64878b495536b036b Mon Sep 17 00:00:00 2001 From: kostabekre Date: Fri, 9 May 2025 15:48:30 +0300 Subject: [PATCH 08/42] fix readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b3e84957..6f654989 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ These default keymaps will only be set if you are in a valid workspace and a mar - `:Obsidian yesterday` to open/create the daily note for the previous working day. -- `:Obsidian index_vault` to manually updated the cache of the vault. (see cache) +- `:Obsidian index_vault` to manually updated the cache of the vault. ### Demo @@ -127,7 +127,7 @@ These default keymaps will only be set if you are in a valid workspace and a mar - Neovim >= 0.10.0 - For completion and search features: - - Backend: [ripgrep](https://github.com/BurntSushi/ripgrep), see [ripgrep#installation](https://github.com/BurntSushi/ripgrep). + - Backend: [ripgrep](https://github.com/BurntSushi/ripgrep), see [ripgrep#installation](https://github.com/BurntSushi/ripgrep) - Frontend: a picker, see [Plugin dependencies](#plugin-dependencies) - Additional system dependencies: From e8052e7e2e4a9c33390ec6400da623cca608ac39 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Fri, 9 May 2025 15:56:12 +0300 Subject: [PATCH 09/42] Updated changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 571277fe..2ca21459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed types in `_snacks.lua` +## [v3.12.0](https://github.com/obsidian-nvim/obsidian.nvim/releases/tag/v3.11.0) + +### Added + +- Added file-watch: code that tracks changed notes in the vault. +- Added cache: a JSON file, which stores aliases, last update date and path to the note. Updated using file-watch. +- Added a new configuration option - `cache`, which is disabled by default. + +### Changed + +- In telescope an option was added to search notes by aliases. + ## [v3.11.0](https://github.com/obsidian-nvim/obsidian.nvim/releases/tag/v3.11.0) - 2025-05-04 ### Added From 043ec034f85bc155a3809fad7de616ef3e1307dc Mon Sep 17 00:00:00 2001 From: kostabekre Date: Fri, 9 May 2025 17:49:59 +0300 Subject: [PATCH 10/42] moved changes in changelog to unreleased --- CHANGELOG.md | 4 ++++ README.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ca21459..a56ce94b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `makefile types` target to check types via lua-ls +- Added file-watch: code that tracks changed notes in the vault. +- Added cache: a JSON file, which stores aliases, last update date and path to the note. Updated using file-watch. +- Added a new configuration option - `cache`, which is disabled by default. ### Changed - Remove `itertools.lua` in favor of `vim.iter` +- In telescope an option was added to search notes by aliases. ### Fixed diff --git a/README.md b/README.md index 6f654989..5fc2d070 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ These default keymaps will only be set if you are in a valid workspace and a mar - `:Obsidian yesterday` to open/create the daily note for the previous working day. -- `:Obsidian index_vault` to manually updated the cache of the vault. +- `:Obsidian index_vault` to manually update the cache of the vault. ### Demo From a39d9d1c18bc5aceff13d259728bc73c814446d1 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Fri, 9 May 2025 17:55:06 +0300 Subject: [PATCH 11/42] removed 3.12 from changelog --- CHANGELOG.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a56ce94b..8e01f691 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,18 +23,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed types in `_snacks.lua` -## [v3.12.0](https://github.com/obsidian-nvim/obsidian.nvim/releases/tag/v3.11.0) - -### Added - -- Added file-watch: code that tracks changed notes in the vault. -- Added cache: a JSON file, which stores aliases, last update date and path to the note. Updated using file-watch. -- Added a new configuration option - `cache`, which is disabled by default. - -### Changed - -- In telescope an option was added to search notes by aliases. - ## [v3.11.0](https://github.com/obsidian-nvim/obsidian.nvim/releases/tag/v3.11.0) - 2025-05-04 ### Added From b80f91386e9f72df06598e2a6254fdc882bf2821 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Fri, 9 May 2025 18:26:24 +0300 Subject: [PATCH 12/42] added semicolon --- test/obsidian/client_spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/obsidian/client_spec.lua b/test/obsidian/client_spec.lua index 4f7f7e9c..a2bc1d1f 100644 --- a/test/obsidian/client_spec.lua +++ b/test/obsidian/client_spec.lua @@ -160,7 +160,7 @@ describe("Client:parse_title_id_path()", function() with_tmp_client(function(client) client.opts.note_path_func = function(_) return "foo-bar-123.md" - end + end; (client.dir / "notes"):mkdir { exist_ok = true } From 8e3df02cf62d6253cdb10d626d9b51692cf448e6 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Sun, 11 May 2025 11:59:02 +0300 Subject: [PATCH 13/42] Updated config option to enable cache --- README.md | 2 +- lua/obsidian/config.lua | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5fc2d070..466703d9 100644 --- a/README.md +++ b/README.md @@ -611,7 +611,7 @@ require("obsidian").setup { -- Experimental feature, disabled by default. cache = { - use_cache = false, + enable = false, cache_path = "./.cache.json", } } diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 00105dd6..c3a61f28 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -535,14 +535,14 @@ end ---@class obsidian.config.CacheOpts --- ----@field use_cache boolean|? Use cache when searching for notes +---@field enable boolean|? Use cache when searching for notes ---@field cache_path string The file where the cache will be saved config.CacheOpts = {} ---@return obsidian.config.CacheOpts config.CacheOpts.default = function() return { - use_cache = false, + enable = false, cache_path = "./.cache.json", } end From a16912ec4f29d7311e538c6a6fde4efe1e24cd67 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Sun, 11 May 2025 11:59:55 +0300 Subject: [PATCH 14/42] Squash merge add-cache into main --- CHANGELOG.md | 4 ++++ README.md | 11 +++++++++++ doc/obsidian_api.txt | 10 ++++++---- lua/obsidian/commands/index_vault.lua | 2 -- lua/obsidian/config.lua | 4 ++-- lua/obsidian/search.lua | 2 -- 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 571277fe..8e01f691 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added `makefile types` target to check types via lua-ls +- Added file-watch: code that tracks changed notes in the vault. +- Added cache: a JSON file, which stores aliases, last update date and path to the note. Updated using file-watch. +- Added a new configuration option - `cache`, which is disabled by default. ### Changed - Remove `itertools.lua` in favor of `vim.iter` +- In telescope an option was added to search notes by aliases. ### Fixed diff --git a/README.md b/README.md index c80933c0..466703d9 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,8 @@ These default keymaps will only be set if you are in a valid workspace and a mar - `:Obsidian yesterday` to open/create the daily note for the previous working day. +- `:Obsidian index_vault` to manually update the cache of the vault. + ### Demo [![2024-01-31 14 22 52](https://github.com/epwalsh/obsidian.nvim/assets/8812459/2986e1d2-13e8-40e2-9c9e-75691a3b662e)](https://github.com/epwalsh/obsidian.nvim/assets/8812459/2986e1d2-13e8-40e2-9c9e-75691a3b662e) @@ -134,6 +136,9 @@ These default keymaps will only be set if you are in a valid workspace and a mar - **MacOS** users need [`pngpaste`](https://github.com/jcsalterego/pngpaste) (`brew install pngpaste`) for `:Obsidian paste_img`. - **Linux** users need xclip (X11) or wl-clipboard (Wayland) for `:Obsidian paste_img`. +- Optional dependencies: + - Backend: [fd](https://github.com/sharkdp/fd), see [fd#installation](https://github.com/sharkdp/fd#installation) for `:Obsidian index_vault` + ### Plugin dependencies The only **required** plugin dependency is [plenary.nvim](https://github.com/nvim-lua/plenary.nvim), but there are a number of optional dependencies that enhance the obsidian.nvim experience. @@ -603,6 +608,12 @@ require("obsidian").setup { enabled = true, format = "{{properties}} properties {{backlinks}} backlinks {{words}} words {{chars}} chars", }, + + -- Experimental feature, disabled by default. + cache = { + enable = false, + cache_path = "./.cache.json", + } } ``` diff --git a/doc/obsidian_api.txt b/doc/obsidian_api.txt index 3c2c323c..206fc1c3 100644 --- a/doc/obsidian_api.txt +++ b/doc/obsidian_api.txt @@ -54,6 +54,7 @@ Class ~ Fields ~ {current_workspace} `(obsidian.Workspace)` The current workspace. {dir} `(obsidian.Path)` The root of the vault for the current workspace. +{cache} `(obsidian.Cache)` {opts} `(obsidian.config.ClientOpts)` The client config. {buf_dir} `(obsidian.Path|? The)` parent directory of the current buffer. {callback_manager} `(obsidian.CallbackManager)` @@ -834,10 +835,11 @@ Return ~ Class ~ {obsidian.note.LoadOpts} Fields ~ -{max_lines} `(integer|)`? -{load_contents} `(boolean|)`? -{collect_anchor_links} `(boolean|)`? -{collect_blocks} `(boolean|)`? +{max_lines} `(integer|? Stops)` reading file if reached max lines. +{read_only_frontmatter} `(boolean|? Stops)` reading file if reached end of frontmatter. +{load_contents} `(boolean|? Save)` contents of the file if not frontmatter or block to obsidian.Note.contents. Still reads whole file if other options are true. +{collect_anchor_links} `(boolean|? Save)` anchor_links of the file if not in frontmatter or block to obsidian.Note.anchor_links. Still reads whole file if other options are true. +{collect_blocks} `(boolean|? Save)` code blocks to obsidian.Note.blocks. Still reads whole file if other options are true. ------------------------------------------------------------------------------ *obsidian.Note.from_file()* diff --git a/lua/obsidian/commands/index_vault.lua b/lua/obsidian/commands/index_vault.lua index 0ebf5975..b4ccfc3a 100644 --- a/lua/obsidian/commands/index_vault.lua +++ b/lua/obsidian/commands/index_vault.lua @@ -1,5 +1,3 @@ -local log = require "obsidian.log" - ---@param client obsidian.Client ---@param data CommandArgs return function(client, data) diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 00105dd6..c3a61f28 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -535,14 +535,14 @@ end ---@class obsidian.config.CacheOpts --- ----@field use_cache boolean|? Use cache when searching for notes +---@field enable boolean|? Use cache when searching for notes ---@field cache_path string The file where the cache will be saved config.CacheOpts = {} ---@return obsidian.config.CacheOpts config.CacheOpts.default = function() return { - use_cache = false, + enable = false, cache_path = "./.cache.json", } end diff --git a/lua/obsidian/search.lua b/lua/obsidian/search.lua index a19af280..d50b86fc 100644 --- a/lua/obsidian/search.lua +++ b/lua/obsidian/search.lua @@ -337,8 +337,6 @@ SearchOpts.to_ripgrep_opts = function(self) opts[#opts + 1] = "--smart-case" end - opts[#opts + 1] = "--color=never" - if self.exclude ~= nil then assert(type(self.exclude) == "table") for path in iter(self.exclude) do From b496be53908bf3c941ba32d006c170a944c9f4e8 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Sun, 11 May 2025 12:06:43 +0300 Subject: [PATCH 15/42] code update --- lua/obsidian/cache.lua | 4 ++-- lua/obsidian/commands/quick_switch.lua | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index 4c9ae190..242cf865 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -197,7 +197,7 @@ Cache.new = function(client) local self = Cache.init() self.client = client - if client.opts.cache.use_cache then + if client.opts.cache.enable then enable_filewatch(self) check_vault_cache(self) @@ -253,7 +253,7 @@ end --- Reads all notes in the vaults and saves them to the cache file. ---@param self obsidian.Cache Cache.index_vault = function(self) - if not self.client.opts.cache.use_cache then + if not self.client.opts.cache.enable then log.error "The cache is disabled. Cannot index vault." end diff --git a/lua/obsidian/commands/quick_switch.lua b/lua/obsidian/commands/quick_switch.lua index edcc8b09..6d1ca79e 100644 --- a/lua/obsidian/commands/quick_switch.lua +++ b/lua/obsidian/commands/quick_switch.lua @@ -10,7 +10,7 @@ return function(client, data) return end - picker:find_notes { use_cache = client.opts.cache.use_cache } + picker:find_notes { use_cache = client.opts.cache.enable } else client:resolve_note_async_with_picker_fallback(data.args, function(note) client:open_note(note) From 7da49ddc5f832a95e87daa5b67a6fbb8e5f9f7e1 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Sun, 11 May 2025 14:38:16 +0300 Subject: [PATCH 16/42] Added log when a note is updated as a warning --- lua/obsidian/filewatch.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index 9d70f5b8..09a53be8 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -1,5 +1,6 @@ local uv = vim.loop local util = require "obsidian.util" +local log = require("obsidian.log") local M = {} @@ -63,8 +64,8 @@ local function watch_path(path, on_event, on_error, opts) end local flags = { - watch_entry = false, -- true = if you pass dir, watch the dir inode only, not the dir content - stat = false, -- true = don't use inotify/kqueue but periodic check, not implemented + watch_entry = false, -- true = if you pass dir, watch the dir inode only, not the dir content + stat = false, -- true = don't use inotify/kqueue but periodic check, not implemented recursive = opts.recursive, -- true = watch dirs inside dirs. For now only works on Windows and MacOS } @@ -99,6 +100,8 @@ local function watch_path(path, on_event, on_error, opts) event_type = M.EventTypes.unknown end + log.warn(table.concat({ "File (", filename, ") was updated at " .. os.date("%H:%M:%S") })) + on_event(full_path, event_type, stat) end) end From 9dc7df8a49cb8dc12ff94c79f7b77249289840a8 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Sun, 11 May 2025 15:37:05 +0300 Subject: [PATCH 17/42] Added a test warning --- lua/obsidian/cache.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index 242cf865..457fa8ec 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -54,6 +54,11 @@ local create_on_file_change_callback = function(self) local links = self:get_cache_notes_from_file() for i, v in ipairs(links) do + if not v then + log.warn("empty note cache is founded") + v = {} + v.absolute_path = "NAN" + end if v.absolute_path == filename then if event_type == EventTypes.deleted then table.remove(links, i) From f7f2aa585766de207bf0d8c92bf925ade9f5f111 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Tue, 13 May 2025 15:45:24 +0300 Subject: [PATCH 18/42] checking get_cache_notes for error --- lua/obsidian/cache.lua | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index 457fa8ec..a0a12adc 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -52,13 +52,15 @@ local create_on_file_change_callback = function(self) vim.schedule(function() local founded = false - local links = self:get_cache_notes_from_file() + local ok, links = pcall(self.get_cache_notes_from_file, self) + + if not ok then + log.err("error when reading from file") + print(vim.inspect(links)) + return + end + for i, v in ipairs(links) do - if not v then - log.warn("empty note cache is founded") - v = {} - v.absolute_path = "NAN" - end if v.absolute_path == filename then if event_type == EventTypes.deleted then table.remove(links, i) From 9714bc3f93976f7f057a39ed05a1606fd5a3d586 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Tue, 13 May 2025 15:49:59 +0300 Subject: [PATCH 19/42] more description of errors --- lua/obsidian/cache.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index a0a12adc..b10f0bac 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -128,7 +128,8 @@ local check_cache_notes_are_fresh = function(self) for _, note_cache in ipairs(note_cache_list) do uv.fs_stat(note_cache.absolute_path, function(err, stat) if err then - err("Couldn't get stat from the file " .. note_cache.relative_path .. " when performing reindex: " .. err) + log.err("Couldn't get stat from the file " .. note_cache.relative_path .. " when performing reindex: " .. err) + return end local aliases From 64f914ebca6012137ddccdb938ae3d27ebe5ad75 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Tue, 13 May 2025 15:53:27 +0300 Subject: [PATCH 20/42] fix error --- lua/obsidian/cache.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index b10f0bac..2f96ddc2 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -128,7 +128,8 @@ local check_cache_notes_are_fresh = function(self) for _, note_cache in ipairs(note_cache_list) do uv.fs_stat(note_cache.absolute_path, function(err, stat) if err then - log.err("Couldn't get stat from the file " .. note_cache.relative_path .. " when performing reindex: " .. err) + -- If the err is occured, the file is deleted, so we don't need to add it to the list. + on_done() return end From 2a2a40decfba76df415728c8fc394d96f11f78b6 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Mon, 19 May 2025 19:51:02 +0300 Subject: [PATCH 21/42] changed log warning to debug --- lua/obsidian/cache.lua | 6 ++++-- lua/obsidian/filewatch.lua | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index 2f96ddc2..ec0942bc 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -45,6 +45,9 @@ local save_cache_notes_to_file = function(links, note_path) end end +---TODO: there can be a possibility, that multiple files will be changed at the same time, which will trigger multiple +---change events, which can lead to data loss. In order to avoid this the filewatcher should return multiple files only after +---some time. ---@param self obsidian.Cache ---@return fun (absolute_path: string, event_type: obsidian.filewatch.EventType, stat: uv.fs_stat.result) local create_on_file_change_callback = function(self) @@ -55,8 +58,7 @@ local create_on_file_change_callback = function(self) local ok, links = pcall(self.get_cache_notes_from_file, self) if not ok then - log.err("error when reading from file") - print(vim.inspect(links)) + log.err("error when reading from the cache file") return end diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index 09a53be8..37f1b9d0 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -100,7 +100,7 @@ local function watch_path(path, on_event, on_error, opts) event_type = M.EventTypes.unknown end - log.warn(table.concat({ "File (", filename, ") was updated at " .. os.date("%H:%M:%S") })) + log.debug(table.concat({ "File (", filename, ") was updated at " .. os.date("%H:%M:%S") })) on_event(full_path, event_type, stat) end) From bd14b1aae92e552f1c4943e11a3138f69f88346a Mon Sep 17 00:00:00 2001 From: kostabekre Date: Wed, 21 May 2025 23:04:02 +0300 Subject: [PATCH 22/42] temp changes --- lua/obsidian/cache.lua | 188 +++++++++++++++++----------- lua/obsidian/filewatch.lua | 2 - lua/obsidian/pickers/_telescope.lua | 72 +++++------ 3 files changed, 150 insertions(+), 112 deletions(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index ec0942bc..80c5b3a3 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -1,3 +1,4 @@ +local async = require "plenary.async" local abc = require "obsidian.abc" local search = require "obsidian.search" local Note = require "obsidian.note" @@ -6,9 +7,8 @@ local EventTypes = require("obsidian.filewatch").EventTypes local uv = vim.uv ---This class allows you to find the notes in your vault more quickly. ----It scans your vault and saves the founded metadata to the default cache file ".cache.json" ----in the root of your vault or in the path you specified. ----For example, this allows to search for alises of your +---It scans your vault and saves the founded metadata to the file specified in your CacheOpts (by default it's ".cache.json"). +---For now, this allows to search for alises of your ---notes in much shorter time. ---@class obsidian.Cache : obsidian.ABC --- @@ -23,22 +23,22 @@ local Cache = abc.new_class() ---@field aliases string[] The alises of the note founded in the frontmatter. ---@field last_updated number The last time the note was updated in seconds since epoch. ----Converts the links to json and saves to the file at the given path. ----@param links obsidian.cache.CacheNote[] ----@param note_path string|obsidian.Note -local save_cache_notes_to_file = function(links, note_path) +---Converts the cache to JSON and saves to the file at the given path. +---@param cache_notes { [string]: obsidian.cache.CacheNote } Dictionary where key is the relative path and value is the cache of the note. +---@param cache_file_path string|obsidian.Note Location to save the cache +local save_cache_notes_to_file = function(cache_notes, cache_file_path) local save_path - if type(note_path) == "obsidian.Note" then - save_path = note_path.path.filename + if type(cache_file_path) == "obsidian.Note" then + save_path = cache_file_path.path.filename else - save_path = note_path + save_path = cache_file_path end local file, err = io.open(save_path, "w") if file then - file:write(vim.fn.json_encode(links)) + file:write(vim.fn.json_encode(cache_notes)) file:close() else error(table.concat { "Couldn't write vault index to the file: ", save_path, ". Description: ", err }) @@ -46,77 +46,95 @@ local save_cache_notes_to_file = function(links, note_path) end ---TODO: there can be a possibility, that multiple files will be changed at the same time, which will trigger multiple ----change events, which can lead to data loss. In order to avoid this the filewatcher should return multiple files only after +---change events, which can lead to performance issues and data loss. In order to avoid this the filewatcher should return multiple files only after ---some time. ---@param self obsidian.Cache ---@return fun (absolute_path: string, event_type: obsidian.filewatch.EventType, stat: uv.fs_stat.result) local create_on_file_change_callback = function(self) - return function(filename, event_type, stat) + return function(absolute_path, event_type, stat) vim.schedule(function() - local founded = false - - local ok, links = pcall(self.get_cache_notes_from_file, self) + local ok, cache_notes = pcall(self.get_cache_notes_from_file, self) if not ok then - log.err("error when reading from the cache file") + log.err("An error occured when reading from the cache file.") return end - for i, v in ipairs(links) do - if v.absolute_path == filename then - if event_type == EventTypes.deleted then - table.remove(links, i) - else - local note = Note.from_file(filename, { read_only_frontmatter = true }) - - local relative_path = note.path.filename:gsub(self.client.dir.filename .. "/", "") - - links[i] = { - absolute_path = filename, - aliases = note.aliases, - relative_path = relative_path, - last_updated = stat.mtime.sec, - } - end + local relative_path = absolute_path:gsub(self.client.dir.filename .. "/", "") - founded = true - break - end - end + ---update the vault when the note is returned + ---@param note obsidian.Note|? + local refresh_vault = function(note) + if note then + local founded_cache = { + absolute_path = absolute_path, + aliases = note.aliases, + relative_path = relative_path, + last_updated = stat.mtime.sec, + } - if not founded then - -- Unknown file that was deleted is not in the cache, so we don't need to do anything. - if event_type == EventTypes.deleted then - return + cache_notes[relative_path] = founded_cache end - local new_note = Note.from_file(filename, { read_only_frontmatter = true }) - - local relative_path = new_note.path.filename:gsub(self.client.dir.filename .. "/", "") - - local new_cache = { - absolute_path = filename, - aliases = new_note.aliases, - relative_path = relative_path, - last_updated = stat.mtime.sec, - } - - table.insert(links, new_cache) + vim.schedule(function() + save_cache_notes_to_file(cache_notes, self.client.opts.cache.cache_path) + end) end - save_cache_notes_to_file(links, self.client.opts.cache.cache_path) + if event_type == EventTypes.deleted and cache_notes[relative_path] then + cache_notes[relative_path] = nil + refresh_vault() + else + async.run(function() + return Note.from_file_async(absolute_path, { read_only_frontmatter = true }) + end, refresh_vault) + end end) end end ----Checks for note cache that were updated outside the vault +---Gets the notes from the vault recursivly +---@param path string Path to a subfolder of the vault. +---@param files string[]|? Founded pathes to notes. +---@return string[] +local function list_notes_recursive(path, files) + files = files or {} + local req = uv.fs_scandir(path) + if not req then return files end + + while true do + local name, type = uv.fs_scandir_next(req) + if not name then break end + if name ~= "." and name ~= ".." then + local full_path = path .. "/" .. name + if type == "directory" then + list_notes_recursive(full_path, files) + elseif type == "file" and full_path:sub(#full_path - 2, #full_path) == ".md" then + table.insert(files, full_path) + end + end + end + + return files +end + +---Gets all notes from the vault. +---@param path string The path to the vault. +---@return string[] The path to the notes. +local function get_all_notes_from_vault(path) + return list_notes_recursive(path) +end + +---Checks for note cache that were updated outside the vault. ---@param self obsidian.Cache local check_cache_notes_are_fresh = function(self) - local note_cache_list = self:get_cache_notes_from_file() + local founded_notes = get_all_notes_from_vault(self.client:vault_root().filename) + local cache_notes = self.get_cache_notes_from_file(self) - local completed = 0 - local total = #note_cache_list + ---@type { [string]: obsidian.cache.CacheNote } local updated = {} + local completed = 0 + local total = #founded_notes local on_done = function() completed = completed + 1 @@ -127,35 +145,42 @@ local check_cache_notes_are_fresh = function(self) end end - for _, note_cache in ipairs(note_cache_list) do - uv.fs_stat(note_cache.absolute_path, function(err, stat) + for _, founded_note in ipairs(founded_notes) do + local relative_path = founded_note:gsub(self.client.dir.filename .. "/", "") + + uv.fs_stat(founded_note, function(err, stat) if err then -- If the err is occured, the file is deleted, so we don't need to add it to the list. on_done() return end + local cache_note = cache_notes[relative_path] + local aliases - if note_cache.last_updated ~= stat.mtime.sec then - local note = Note.from_file(note_cache.absolute_path, { read_only_frontmatter = true }) - aliases = note.aliases + if cache_note and cache_note.last_updated == stat.mtime.sec then + aliases = cache_note.aliases else - aliases = note_cache.aliases + local note = Note.from_file(founded_note, { read_only_frontmatter = true }) + aliases = note.aliases end ---@type obsidian.cache.CacheNote - table.insert(updated, { - absolute_path = note_cache.absolute_path, + local updated_cache = { + absolute_path = founded_note, last_updated = stat.mtime.sec, aliases = aliases, - relative_path = note_cache.relative_path, - }) + relative_path = relative_path, + } + + updated[relative_path] = updated_cache on_done() end) end end + ---Checks that file exits ---@param path string ---@param callback fun (result: boolean) @@ -219,17 +244,18 @@ end --- Reads all notes in the vaults and returns the founded data. ---@param client obsidian.Client ----@return obsidian.cache.CacheNote[] +---@return { [string]: obsidian.cache.CacheNote } local get_cache_notes_from_vault = function(client) local interator = search.find(client.dir, "", nil) - ---@type obsidian.cache.CacheNote[] + ---@type { [string]: obsidian.cache.CacheNote } local created_note_caches = {} local notepath = interator() --TODO add indexing progress while notepath do + --TODO make async local note = Note.from_file(notepath, { read_only_frontmatter = true }) local absolute_path = note.path.filename @@ -253,7 +279,7 @@ local get_cache_notes_from_vault = function(client) last_updated = last_updated, } - table.insert(created_note_caches, note_cache) + created_note_caches[relative_path] = note_cache notepath = interator() end @@ -275,9 +301,9 @@ Cache.index_vault = function(self) log.info "Vault was indexed succesfully." end ----Reads the cache file from client.opts.cache.cache_path and returns founded note cache. +---Reads the cache file from client.opts.cache.cache_path and returns the loaded cache. ---@param self obsidian.Cache ----@return obsidian.cache.CacheNote[] +---@return { [string]: obsidian.cache.CacheNote } Key is the relative path to the vault, value is the cache of the note. Cache.get_cache_notes_from_file = function(self) local file, err = io.open(self.client.opts.cache.cache_path, "r") @@ -286,8 +312,22 @@ Cache.get_cache_notes_from_file = function(self) file:close() return vim.fn.json_decode(links_json) else - error("couldn't read vault index from file: " .. err) + print(err) + end +end + +---Reads the cache file from client.opts.cache.cache_path and returns founded note cache without key. +---@param self obsidian.Cache +---@return obsidian.cache.CacheNote[] +Cache.get_cache_notes_without_key = function(self) + local cache_with_index = self:get_cache_notes_from_file() + + local cache_without_index = {} + for _, value in pairs(cache_with_index) do + table.insert(cache_without_index, value) end + + return cache_without_index end return Cache diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index 37f1b9d0..1ab183e8 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -100,8 +100,6 @@ local function watch_path(path, on_event, on_error, opts) event_type = M.EventTypes.unknown end - log.debug(table.concat({ "File (", filename, ") was updated at " .. os.date("%H:%M:%S") })) - on_event(full_path, event_type, stat) end) end diff --git a/lua/obsidian/pickers/_telescope.lua b/lua/obsidian/pickers/_telescope.lua index 80fec486..444d85f4 100644 --- a/lua/obsidian/pickers/_telescope.lua +++ b/lua/obsidian/pickers/_telescope.lua @@ -170,7 +170,7 @@ local create_cache_picker = function(self, prompt_title, opts) return pickers.new(picker_opts, { cwd = opts.dir, finder = finders.new_table { - results = self.client.cache:get_cache_notes_from_file(), + results = self.client.cache:get_cache_notes_without_key(), ---@param entry obsidian.cache.CacheNote ---@return obsidian.pickers.telescope_picker.CacheSelectedEntry entry_maker = function(entry) @@ -309,7 +309,7 @@ TelescopePicker.pick = function(self, values, opts) local picker_conf = conf.pickers[picker_name] if picker_conf and picker_conf.theme then picker_opts = - vim.tbl_extend("force", picker_opts, require("telescope.themes")["get_" .. picker_conf.theme] {}) + vim.tbl_extend("force", picker_opts, require("telescope.themes")["get_" .. picker_conf.theme] {}) break end end @@ -319,42 +319,42 @@ TelescopePicker.pick = function(self, values, opts) local make_entry_from_string = make_entry.gen_from_string(picker_opts) pickers - .new(picker_opts, { - prompt_title = prompt_title, - finder = finders.new_table { - results = values, - entry_maker = function(v) - if type(v) == "string" then - return make_entry_from_string(v) - else - local ordinal = v.ordinal - if ordinal == nil then - ordinal = "" - if type(v.display) == "string" then - ordinal = ordinal .. v.display - end - if v.filename ~= nil then - ordinal = ordinal .. " " .. v.filename + .new(picker_opts, { + prompt_title = prompt_title, + finder = finders.new_table { + results = values, + entry_maker = function(v) + if type(v) == "string" then + return make_entry_from_string(v) + else + local ordinal = v.ordinal + if ordinal == nil then + ordinal = "" + if type(v.display) == "string" then + ordinal = ordinal .. v.display + end + if v.filename ~= nil then + ordinal = ordinal .. " " .. v.filename + end end - end - return { - value = v.value, - display = displayer, - ordinal = ordinal, - filename = v.filename, - valid = v.valid, - lnum = v.lnum, - col = v.col, - raw = v, - } - end - end, - }, - sorter = conf.values.generic_sorter(picker_opts), - previewer = previewer, - }) - :find() + return { + value = v.value, + display = displayer, + ordinal = ordinal, + filename = v.filename, + valid = v.valid, + lnum = v.lnum, + col = v.col, + raw = v, + } + end + end, + }, + sorter = conf.values.generic_sorter(picker_opts), + previewer = previewer, + }) + :find() end return TelescopePicker From 8d80abb29dfc17b4e2b8e03147e23357a1539288 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Thu, 22 May 2025 22:37:11 +0300 Subject: [PATCH 23/42] file watch sends multiple files but didn't test --- lua/obsidian/cache.lua | 100 +++++++++++++++---------------------- lua/obsidian/filewatch.lua | 82 ++++++++++++++++++++---------- lua/obsidian/os_util.lua | 57 +++++++++++++++++++++ 3 files changed, 151 insertions(+), 88 deletions(-) create mode 100644 lua/obsidian/os_util.lua diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index 80c5b3a3..9007ba30 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -5,6 +5,7 @@ local Note = require "obsidian.note" local log = require "obsidian.log" local EventTypes = require("obsidian.filewatch").EventTypes local uv = vim.uv +local os_util = require("obsidian.os_util") ---This class allows you to find the notes in your vault more quickly. ---It scans your vault and saves the founded metadata to the file specified in your CacheOpts (by default it's ".cache.json"). @@ -45,13 +46,11 @@ local save_cache_notes_to_file = function(cache_notes, cache_file_path) end end ----TODO: there can be a possibility, that multiple files will be changed at the same time, which will trigger multiple ----change events, which can lead to performance issues and data loss. In order to avoid this the filewatcher should return multiple files only after ----some time. +---Update the cache file for changed files. ---@param self obsidian.Cache ----@return fun (absolute_path: string, event_type: obsidian.filewatch.EventType, stat: uv.fs_stat.result) +---@return fun (changed_files: obsidian.filewatch.CallbackArgs[]) local create_on_file_change_callback = function(self) - return function(absolute_path, event_type, stat) + return function(changed_files) vim.schedule(function() local ok, cache_notes = pcall(self.get_cache_notes_from_file, self) @@ -60,75 +59,54 @@ local create_on_file_change_callback = function(self) return end - local relative_path = absolute_path:gsub(self.client.dir.filename .. "/", "") - - ---update the vault when the note is returned - ---@param note obsidian.Note|? - local refresh_vault = function(note) - if note then - local founded_cache = { - absolute_path = absolute_path, - aliases = note.aliases, - relative_path = relative_path, - last_updated = stat.mtime.sec, - } - - cache_notes[relative_path] = founded_cache - end - + local update_cache_file = function() vim.schedule(function() save_cache_notes_to_file(cache_notes, self.client.opts.cache.cache_path) end) end - if event_type == EventTypes.deleted and cache_notes[relative_path] then - cache_notes[relative_path] = nil - refresh_vault() - else - async.run(function() - return Note.from_file_async(absolute_path, { read_only_frontmatter = true }) - end, refresh_vault) - end - end) - end -end + local left = #changed_files ----Gets the notes from the vault recursivly ----@param path string Path to a subfolder of the vault. ----@param files string[]|? Founded pathes to notes. ----@return string[] -local function list_notes_recursive(path, files) - files = files or {} - local req = uv.fs_scandir(path) - if not req then return files end - - while true do - local name, type = uv.fs_scandir_next(req) - if not name then break end - if name ~= "." and name ~= ".." then - local full_path = path .. "/" .. name - if type == "directory" then - list_notes_recursive(full_path, files) - elseif type == "file" and full_path:sub(#full_path - 2, #full_path) == ".md" then - table.insert(files, full_path) - end - end - end + for _, file in ipairs(changed_files) do + local relative_path = file.absolute_path:gsub(self.client.dir.filename .. "/", "") - return files -end + ---@param note obsidian.Note|? + local update_cache_dictionary = function(note) + if note then + local founded_cache = { + absolute_path = absolute_path, + aliases = note.aliases, + relative_path = relative_path, + last_updated = file.stat.mtime.sec, + } + + cache_notes[relative_path] = founded_cache + end ----Gets all notes from the vault. ----@param path string The path to the vault. ----@return string[] The path to the notes. -local function get_all_notes_from_vault(path) - return list_notes_recursive(path) + left = left - 1 + + if left == 0 then + update_cache_file() + end + end + + if file.event == EventTypes.deleted and cache_notes[relative_path] then + cache_notes[relative_path] = nil + update_cache_dictionary() + else + async.run(function() + return Note.from_file_async(file.absolute_path, { read_only_frontmatter = true }) + end, update_cache_dictionary) + end + end + end) + end end ---Checks for note cache that were updated outside the vault. ---@param self obsidian.Cache local check_cache_notes_are_fresh = function(self) - local founded_notes = get_all_notes_from_vault(self.client:vault_root().filename) + local founded_notes = os_util.get_all_notes_from_vault(self.client:vault_root().filename) local cache_notes = self.get_cache_notes_from_file(self) ---@type { [string]: obsidian.cache.CacheNote } diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index 1ab183e8..38ba08cf 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -1,10 +1,21 @@ local uv = vim.loop local util = require "obsidian.util" -local log = require("obsidian.log") +local os_util = require("obsidian.os_util") local M = {} ----@class obsidian.filewatch.FileWatchOpts +---@enum obsidian.filewatch.EventType +M.EventTypes = { + unknown = 0, + changed = 1, + renamed = 2, + deleted = 3, +} + +---@class obsidian.filewatch.CallbackArgs +---@field absolute_path string The absolute path to the changed file. +---@field event obsidian.filewatch.EventType The type of the event. +---@field stat uv.fs_stat.result|? The uv file info. ---Creates default callback if error occured in fs_event_start. ---@param path string The filepath where error occured. @@ -15,16 +26,10 @@ local make_default_error_cb = function(path) end end ----@enum obsidian.filewatch.EventType -M.EventTypes = { - unknown = 0, - changed = 1, - renamed = 2, - deleted = 3, -} - ---- Minimal time in milliseconds to allow the event to fire. +--- Minimal time in milliseconds to allow the event to fire for a single file. local MIN_INTERVAL = 50 +--- The time in milleseconds when the changed files will be send to the client. +local CALLBACK_AFTER_INTERVAL = 500 ---Check if the event is not a duplicate or the received name is not `~` or a number. ---@param filename string @@ -52,7 +57,7 @@ end --- Watch path and calls on_event(filename, event_type) or on_error(error) ---@param path string ----@param on_event fun (absolute_path: string, event_type: obsidian.filewatch.EventType, stat: uv.fs_stat.result|?) +---@param on_event fun (changed_files: obsidian.filewatch.CallbackArgs[]) ---@param on_error fun (err: string) ---@param opts {recursive: boolean} ---@return uv.uv_fs_event_t @@ -69,7 +74,27 @@ local function watch_path(path, on_event, on_error, opts) recursive = opts.recursive, -- true = watch dirs inside dirs. For now only works on Windows and MacOS } + ---@type {[string]: number|?} local last_received_files = {} + ---@type obsidian.filewatch.CallbackArgs[] + local queue_to_send = {} + local queue_timer = uv.new_timer() + if not queue_timer then + error("Couldn't create queue timer!") + end + + ---Tracks the changed files and returns them to the client after some time. + ---@param send_arg obsidian.filewatch.CallbackArgs + local add_to_queue = function(send_arg) + table.insert(queue_to_send, send_arg) + + if #queue_to_send == 0 then + queue_timer:start(CALLBACK_AFTER_INTERVAL, 0, function() + on_event(queue_to_send) + queue_to_send = {} + end) + end + end local event_cb = function(err, filename, events) if err then @@ -83,11 +108,15 @@ local function watch_path(path, on_event, on_error, opts) local folder_path = uv.fs_event_getpath(handle) - local full_path = table.concat { folder_path, "/", filename } + local full_path = table.concat { folder_path, filename } uv.fs_stat(full_path, function(stat_err, stat) if stat_err then - on_event(full_path, M.EventTypes.deleted, nil) + on_event({ + absolute_path = full_path, + event = M.EventTypes.deleted, + stat = nil + }) return end @@ -100,7 +129,11 @@ local function watch_path(path, on_event, on_error, opts) event_type = M.EventTypes.unknown end - on_event(full_path, event_type, stat) + add_to_queue({ + absolute_path = full_path, + event = event_type, + stat = stat + }) end) end @@ -113,11 +146,12 @@ local function watch_path(path, on_event, on_error, opts) return handle end ----Create a watch handler which uses callback function when a file is changed. +---Create a watch handler (several if on Linux) which calls the callback function when a file is changed. +---Calls the callback function only after CALLBACK_AFTER_INTERVAL. ---If an error occured, called on_error function. ----TODO if a new folder will be created, it won't be tracked ----@param path string ----@param callback fun (absolute_path: string, event_type: obsidian.filewatch.EventType, stat: uv.fs_stat.result|?) +---TODO if a new folder will be created, it won't be tracked. +---@param path string The path to the watch folder. +---@param callback fun (changed_files: obsidian.filewatch.CallbackArgs[]) ---@param on_error fun (err: string)|? ---@return uv.uv_fs_event_t[] M.watch = function(path, callback, on_error) @@ -135,20 +169,14 @@ M.watch = function(path, callback, on_error) local sysname = util.get_os() + -- uv doesn't support recursive flag on Linux if sysname == util.OSType.Linux then - local handle = io.popen("fd -t directory -a --base-directory " .. path) - if not handle then - error "Failed to execute command" - end - local subdirs_handlers = {} - for dir in handle:lines() do + for dir in os_util.get_sub_dirs_from_vault(path) do table.insert(subdirs_handlers, watch_path(dir, callback, on_error, { recursive = false })) end - handle:close() - return subdirs_handlers else return { watch_path(path, callback, on_error, { recursive = true }) } diff --git a/lua/obsidian/os_util.lua b/lua/obsidian/os_util.lua new file mode 100644 index 00000000..78195160 --- /dev/null +++ b/lua/obsidian/os_util.lua @@ -0,0 +1,57 @@ +local uv = vim.uv + +local M = {} + +---Gets the notes from the vault recursivly +---@param path string Path to a subfolder of the vault. +---@param files string[]|? Founded pathes to notes. +---@return string[] +local function list_notes_recursive(path, files) + files = files or {} + local req = uv.fs_scandir(path) + if not req then return files end + + while true do + local name, type = uv.fs_scandir_next(req) + if not name then break end + if name ~= "." and name ~= ".." then + local full_path = path .. "/" .. name + if type == "directory" then + list_notes_recursive(full_path, files) + elseif type == "file" and full_path:sub(#full_path - 2, #full_path) == ".md" then + table.insert(files, full_path) + end + end + end + + return files +end + +---Gets all notes from the vault. +---@param path string The path to the vault. +---@return string[] The path to the notes. +M.get_all_notes_from_vault = function(path) + return list_notes_recursive(path) +end + +---Gets all subfolders from the vault. +---@param path string The path to the vault. +---@return string[] The path to the subfolders +M.get_sub_dirs_from_vault = function(path) + local handle = io.popen("fd -t directory -a --base-directory " .. path) + if not handle then + error "Failed to execute command" + end + + local subdirs = {} + + for dir in handle:lines() do + table.insert(subdirs, dir) + end + + handle:close() + + return subdirs +end + +return M From 040e7661c352b483e92ba9b058c830291532b987 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Mon, 2 Jun 2025 16:26:41 +0300 Subject: [PATCH 24/42] Fixed errors --- lua/obsidian/cache.lua | 12 ++------ lua/obsidian/filewatch.lua | 58 +++++++++++++++++++++++++------------- lua/obsidian/os_util.lua | 42 +++++++++------------------ 3 files changed, 56 insertions(+), 56 deletions(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index 9007ba30..ff55c1c3 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -175,18 +175,12 @@ end ---Watches the vault for changes. ---@param self obsidian.Cache local enable_filewatch = function(self) - local handlers = require("obsidian.filewatch").watch(self.client.dir.filename, create_on_file_change_callback(self)) + local filewatch = require("obsidian.filewatch") + filewatch.watch(self.client.dir.filename, create_on_file_change_callback(self)) vim.api.nvim_create_autocmd({ "QuitPre", "ExitPre" }, { callback = function() - for _, handle in ipairs(handlers) do - if handle then - handle:stop() - if not handle.is_closing then - handle:close() - end - end - end + filewatch.release_resources() end, }) end diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index 38ba08cf..bee9fb66 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -31,6 +31,13 @@ local MIN_INTERVAL = 50 --- The time in milleseconds when the changed files will be send to the client. local CALLBACK_AFTER_INTERVAL = 500 +---@type obsidian.filewatch.CallbackArgs[] +local queue_to_send = {} +---@type uv.uv_timer_t +local queue_timer +---@type uv.uv_fs_event_t[] +local watch_handlers = {} + ---Check if the event is not a duplicate or the received name is not `~` or a number. ---@param filename string ---@param last_received_files {[string]: number|?} @@ -76,24 +83,18 @@ local function watch_path(path, on_event, on_error, opts) ---@type {[string]: number|?} local last_received_files = {} - ---@type obsidian.filewatch.CallbackArgs[] - local queue_to_send = {} - local queue_timer = uv.new_timer() - if not queue_timer then - error("Couldn't create queue timer!") - end ---Tracks the changed files and returns them to the client after some time. ---@param send_arg obsidian.filewatch.CallbackArgs local add_to_queue = function(send_arg) table.insert(queue_to_send, send_arg) - if #queue_to_send == 0 then - queue_timer:start(CALLBACK_AFTER_INTERVAL, 0, function() - on_event(queue_to_send) - queue_to_send = {} - end) - end + queue_timer:stop(); + + queue_timer:start(CALLBACK_AFTER_INTERVAL, 0, function() + on_event(queue_to_send) + queue_to_send = {} + end) end local event_cb = function(err, filename, events) @@ -153,7 +154,6 @@ end ---@param path string The path to the watch folder. ---@param callback fun (changed_files: obsidian.filewatch.CallbackArgs[]) ---@param on_error fun (err: string)|? ----@return uv.uv_fs_event_t[] M.watch = function(path, callback, on_error) if not path or path == "" then error "Path cannot be empty." @@ -167,19 +167,39 @@ M.watch = function(path, callback, on_error) on_error = make_default_error_cb(path) end + local new_timer = uv.new_timer() + + if not new_timer then + error("Couldn't create queue timer!") + end + + queue_timer = new_timer + local sysname = util.get_os() -- uv doesn't support recursive flag on Linux if sysname == util.OSType.Linux then - local subdirs_handlers = {} + for _, dir in ipairs(os_util.get_sub_dirs_from_vault(path)) do + table.insert(watch_handlers, watch_path(dir, callback, on_error, { recursive = false })) + end + else + watch_handlers = { watch_path(path, callback, on_error, { recursive = true }) } + end +end - for dir in os_util.get_sub_dirs_from_vault(path) do - table.insert(subdirs_handlers, watch_path(dir, callback, on_error, { recursive = false })) +M.release_resources = function() + for _, handle in ipairs(watch_handlers) do + if handle then + handle:stop() + if not handle.is_closing then + handle:close() + end end + end - return subdirs_handlers - else - return { watch_path(path, callback, on_error, { recursive = true }) } + queue_timer:stop() + if not queue_timer.is_closing then + queue_timer:close() end end diff --git a/lua/obsidian/os_util.lua b/lua/obsidian/os_util.lua index 78195160..839c5de1 100644 --- a/lua/obsidian/os_util.lua +++ b/lua/obsidian/os_util.lua @@ -1,37 +1,23 @@ -local uv = vim.uv - local M = {} ----Gets the notes from the vault recursivly ----@param path string Path to a subfolder of the vault. ----@param files string[]|? Founded pathes to notes. ----@return string[] -local function list_notes_recursive(path, files) - files = files or {} - local req = uv.fs_scandir(path) - if not req then return files end - - while true do - local name, type = uv.fs_scandir_next(req) - if not name then break end - if name ~= "." and name ~= ".." then - local full_path = path .. "/" .. name - if type == "directory" then - list_notes_recursive(full_path, files) - elseif type == "file" and full_path:sub(#full_path - 2, #full_path) == ".md" then - table.insert(files, full_path) - end - end - end - - return files -end - ---Gets all notes from the vault. ---@param path string The path to the vault. ---@return string[] The path to the notes. M.get_all_notes_from_vault = function(path) - return list_notes_recursive(path) + local handle = io.popen("fd -t file -a --base-directory " .. path) + if not handle then + error "Failed to execute command" + end + + local files = {} + + for file in handle:lines() do + table.insert(files, file) + end + + handle:close() + + return files end ---Gets all subfolders from the vault. From 2a37f1d1f2db2055e57d731e227c6b0bc5a93dd0 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Mon, 2 Jun 2025 17:00:25 +0300 Subject: [PATCH 25/42] Updated Cache config --- README.md | 2 +- lua/obsidian/cache.lua | 14 +++++++------- lua/obsidian/config.lua | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a1ad76ee..88939537 100644 --- a/README.md +++ b/README.md @@ -626,7 +626,7 @@ require("obsidian").setup { -- Experimental feature, disabled by default. cache = { enable = false, - cache_path = "./.cache.json", + path = "./.cache.json", } } ``` diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index ff55c1c3..403e28a6 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -61,7 +61,7 @@ local create_on_file_change_callback = function(self) local update_cache_file = function() vim.schedule(function() - save_cache_notes_to_file(cache_notes, self.client.opts.cache.cache_path) + save_cache_notes_to_file(cache_notes, self.client.opts.cache.path) end) end @@ -118,7 +118,7 @@ local check_cache_notes_are_fresh = function(self) if completed == total then vim.schedule(function() - save_cache_notes_to_file(updated, self.client.opts.cache.cache_path) + save_cache_notes_to_file(updated, self.client.opts.cache.path) end) end end @@ -187,7 +187,7 @@ end ---@param self obsidian.Cache local check_vault_cache = function(self) - check_file_exists(self.client.opts.cache.cache_path, function(exists) + check_file_exists(self.client.opts.cache.path, function(exists) if exists then vim.schedule(function() check_cache_notes_are_fresh(self) @@ -268,16 +268,16 @@ Cache.index_vault = function(self) local founded_links = get_cache_notes_from_vault(self.client) - save_cache_notes_to_file(founded_links, self.client.opts.cache.cache_path) + save_cache_notes_to_file(founded_links, self.client.opts.cache.path) log.info "Vault was indexed succesfully." end ----Reads the cache file from client.opts.cache.cache_path and returns the loaded cache. +---Reads the cache file from client.opts.cache.path and returns the loaded cache. ---@param self obsidian.Cache ---@return { [string]: obsidian.cache.CacheNote } Key is the relative path to the vault, value is the cache of the note. Cache.get_cache_notes_from_file = function(self) - local file, err = io.open(self.client.opts.cache.cache_path, "r") + local file, err = io.open(self.client.opts.cache.path, "r") if file then local links_json = file:read() @@ -288,7 +288,7 @@ Cache.get_cache_notes_from_file = function(self) end end ----Reads the cache file from client.opts.cache.cache_path and returns founded note cache without key. +---Reads the cache file from client.opts.cache.path and returns founded note cache without key. ---@param self obsidian.Cache ---@return obsidian.cache.CacheNote[] Cache.get_cache_notes_without_key = function(self) diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index a5954c34..d0daf22a 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -577,14 +577,14 @@ end ---@class obsidian.config.CacheOpts --- ---@field enable boolean|? Use cache when searching for notes ----@field cache_path string The file where the cache will be saved +---@field path string The file where the cache will be saved config.CacheOpts = {} ---@return obsidian.config.CacheOpts config.CacheOpts.default = function() return { enable = false, - cache_path = "./.cache.json", + path = "./.cache.json", } end From b19e938fb5239e38ba52d7979efe6e54c358d69f Mon Sep 17 00:00:00 2001 From: kostabekre Date: Sun, 15 Jun 2025 11:24:12 +0300 Subject: [PATCH 26/42] Added the file handle for root on Linux --- lua/obsidian/filewatch.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index bee9fb66..0feb3e72 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -179,6 +179,8 @@ M.watch = function(path, callback, on_error) -- uv doesn't support recursive flag on Linux if sysname == util.OSType.Linux then + table.insert(watch_handlers, watch_path(path, callback, on_error, { recursive = false })) + for _, dir in ipairs(os_util.get_sub_dirs_from_vault(path)) do table.insert(watch_handlers, watch_path(dir, callback, on_error, { recursive = false })) end From 9da52d31724be49b032c3484503d01f74376ff3c Mon Sep 17 00:00:00 2001 From: kostabekre Date: Sun, 15 Jun 2025 13:25:06 +0300 Subject: [PATCH 27/42] github code review changes --- README.md | 4 +-- lua/obsidian/cache.lua | 43 +++++++++++---------------- lua/obsidian/commands/index_vault.lua | 2 +- lua/obsidian/commands/init.lua | 4 +-- lua/obsidian/os_util.lua | 7 +++-- lua/obsidian/pickers/_telescope.lua | 6 ++-- 6 files changed, 32 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 88939537..c06a64f5 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ These default keymaps will only be set if you are in a valid workspace and a mar - `:Obsidian yesterday` to open/create the daily note for the previous working day. -- `:Obsidian index_vault` to manually update the cache of the vault. +- `:Obsidian rebuild_cache` to manually update the cache of the workspace. ### Demo @@ -142,7 +142,7 @@ These default keymaps will only be set if you are in a valid workspace and a mar - **Linux** users need xclip (X11) or wl-clipboard (Wayland) for `:Obsidian paste_img`. - Optional dependencies: - - Backend: [fd](https://github.com/sharkdp/fd), see [fd#installation](https://github.com/sharkdp/fd#installation) for `:Obsidian index_vault` + - Backend: [fd](https://github.com/sharkdp/fd), see [fd#installation](https://github.com/sharkdp/fd#installation) for `:Obsidian rebuild_cache` ### Plugin dependencies diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index 403e28a6..a2bbb48e 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -20,7 +20,6 @@ local Cache = abc.new_class() ---@class obsidian.cache.CacheNote --- ---@field absolute_path string The full path to the note. ----@field relative_path string The relative path to the root of the vault. ---@field aliases string[] The alises of the note founded in the frontmatter. ---@field last_updated number The last time the note was updated in seconds since epoch. @@ -52,10 +51,9 @@ end local create_on_file_change_callback = function(self) return function(changed_files) vim.schedule(function() - local ok, cache_notes = pcall(self.get_cache_notes_from_file, self) + local cache_notes = self:get_cache_notes_from_file() - if not ok then - log.err("An error occured when reading from the cache file.") + if not cache_notes then return end @@ -76,7 +74,6 @@ local create_on_file_change_callback = function(self) local founded_cache = { absolute_path = absolute_path, aliases = note.aliases, - relative_path = relative_path, last_updated = file.stat.mtime.sec, } @@ -107,7 +104,11 @@ end ---@param self obsidian.Cache local check_cache_notes_are_fresh = function(self) local founded_notes = os_util.get_all_notes_from_vault(self.client:vault_root().filename) - local cache_notes = self.get_cache_notes_from_file(self) + local cache_notes = self:get_cache_notes_from_file() + + if not cache_notes or not founded_notes then + return + end ---@type { [string]: obsidian.cache.CacheNote } local updated = {} @@ -148,7 +149,6 @@ local check_cache_notes_are_fresh = function(self) absolute_path = founded_note, last_updated = stat.mtime.sec, aliases = aliases, - relative_path = relative_path, } updated[relative_path] = updated_cache @@ -164,11 +164,7 @@ end ---@param callback fun (result: boolean) local check_file_exists = function(path, callback) uv.fs_stat(path, function(err, _) - if not err then - callback(true) - else - callback(false) - end + callback(err == nil) end) end @@ -194,7 +190,7 @@ local check_vault_cache = function(self) end) else vim.schedule(function() - self:index_vault() + self:rebuild_cache() end) end end) @@ -218,15 +214,10 @@ end ---@param client obsidian.Client ---@return { [string]: obsidian.cache.CacheNote } local get_cache_notes_from_vault = function(client) - local interator = search.find(client.dir, "", nil) - ---@type { [string]: obsidian.cache.CacheNote } local created_note_caches = {} - local notepath = interator() - - --TODO add indexing progress - while notepath do + for notepath in search.find(client.dir, "", nil) do --TODO make async local note = Note.from_file(notepath, { read_only_frontmatter = true }) @@ -247,13 +238,10 @@ local get_cache_notes_from_vault = function(client) local note_cache = { absolute_path = absolute_path, aliases = note.aliases, - relative_path = relative_path, last_updated = last_updated, } created_note_caches[relative_path] = note_cache - - notepath = interator() end return created_note_caches @@ -261,7 +249,7 @@ end --- Reads all notes in the vaults and saves them to the cache file. ---@param self obsidian.Cache -Cache.index_vault = function(self) +Cache.rebuild_cache = function(self) if not self.client.opts.cache.enable then log.error "The cache is disabled. Cannot index vault." end @@ -275,7 +263,7 @@ end ---Reads the cache file from client.opts.cache.path and returns the loaded cache. ---@param self obsidian.Cache ----@return { [string]: obsidian.cache.CacheNote } Key is the relative path to the vault, value is the cache of the note. +---@return { [string]: obsidian.cache.CacheNote }|? Key is the relative path to the vault, value is the cache of the note. Cache.get_cache_notes_from_file = function(self) local file, err = io.open(self.client.opts.cache.path, "r") @@ -284,7 +272,8 @@ Cache.get_cache_notes_from_file = function(self) file:close() return vim.fn.json_decode(links_json) else - print(err) + log.err(err) + return nil end end @@ -294,6 +283,10 @@ end Cache.get_cache_notes_without_key = function(self) local cache_with_index = self:get_cache_notes_from_file() + if not cache_with_index then + return {} + end + local cache_without_index = {} for _, value in pairs(cache_with_index) do table.insert(cache_without_index, value) diff --git a/lua/obsidian/commands/index_vault.lua b/lua/obsidian/commands/index_vault.lua index b4ccfc3a..78059ffa 100644 --- a/lua/obsidian/commands/index_vault.lua +++ b/lua/obsidian/commands/index_vault.lua @@ -1,5 +1,5 @@ ---@param client obsidian.Client ---@param data CommandArgs return function(client, data) - client.cache:index_vault() + client.cache:rebuild_cache() end diff --git a/lua/obsidian/commands/init.lua b/lua/obsidian/commands/init.lua index 1f2fbbce..59095a6a 100644 --- a/lua/obsidian/commands/init.lua +++ b/lua/obsidian/commands/init.lua @@ -17,7 +17,7 @@ local cmds = { "open", "paste_img", "quick_switch", - "index_vault", + "rebuild_cache", "rename", "search", "tags", @@ -226,7 +226,7 @@ M.register("new_from_template", { nargs = "*" }) M.register("quick_switch", { nargs = "?" }) -M.register("index_vault", { nargs = "?" }) +M.register("rebuild_cache", { nargs = "?" }) M.register("link_new", { nargs = "?", range = true }) diff --git a/lua/obsidian/os_util.lua b/lua/obsidian/os_util.lua index 839c5de1..538acf32 100644 --- a/lua/obsidian/os_util.lua +++ b/lua/obsidian/os_util.lua @@ -1,12 +1,15 @@ +local log = require "obsidian.log" + local M = {} ---Gets all notes from the vault. ---@param path string The path to the vault. ----@return string[] The path to the notes. +---@return string[]|? The path to the notes. M.get_all_notes_from_vault = function(path) local handle = io.popen("fd -t file -a --base-directory " .. path) if not handle then - error "Failed to execute command" + log.err "Failed to execute command" + return nil end local files = {} diff --git a/lua/obsidian/pickers/_telescope.lua b/lua/obsidian/pickers/_telescope.lua index 444d85f4..6938708c 100644 --- a/lua/obsidian/pickers/_telescope.lua +++ b/lua/obsidian/pickers/_telescope.lua @@ -175,11 +175,13 @@ local create_cache_picker = function(self, prompt_title, opts) ---@return obsidian.pickers.telescope_picker.CacheSelectedEntry entry_maker = function(entry) local concated_aliases = table.concat(entry.aliases, "|") + local relative_path = entry.absolute_path:gsub(self.client.dir.filename .. "/", "") local display_name + if concated_aliases and concated_aliases ~= "" then - display_name = table.concat({ entry.relative_path, concated_aliases }, "|") + display_name = table.concat({ relative_path, concated_aliases }, "|") else - display_name = entry.relative_path + display_name = relative_path end return { From 5bef2a6add8fef32214a9154115a6ea99bf67b93 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Sun, 15 Jun 2025 17:20:54 +0300 Subject: [PATCH 28/42] github code review --- README.md | 2 +- lua/obsidian/cache.lua | 84 ++++++++++--------- lua/obsidian/commands/quick_switch.lua | 2 +- .../{index_vault.lua => rebuild_cache.lua} | 0 lua/obsidian/config.lua | 9 +- 5 files changed, 50 insertions(+), 47 deletions(-) rename lua/obsidian/commands/{index_vault.lua => rebuild_cache.lua} (100%) diff --git a/README.md b/README.md index 673fc62c..f2b914d4 100644 --- a/README.md +++ b/README.md @@ -629,7 +629,7 @@ require("obsidian").setup { -- Experimental feature, disabled by default. cache = { - enable = false, + enabled = false, path = "./.cache.json", } } diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index a2bbb48e..697cab68 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -1,6 +1,7 @@ local async = require "plenary.async" local abc = require "obsidian.abc" local search = require "obsidian.search" +local channel = require("plenary.async.control").channel local Note = require "obsidian.note" local log = require "obsidian.log" local EventTypes = require("obsidian.filewatch").EventTypes @@ -25,20 +26,12 @@ local Cache = abc.new_class() ---Converts the cache to JSON and saves to the file at the given path. ---@param cache_notes { [string]: obsidian.cache.CacheNote } Dictionary where key is the relative path and value is the cache of the note. ----@param cache_file_path string|obsidian.Note Location to save the cache +---@param cache_file_path string Location to save the cache local save_cache_notes_to_file = function(cache_notes, cache_file_path) - local save_path - - if type(cache_file_path) == "obsidian.Note" then - save_path = cache_file_path.path.filename - else - save_path = cache_file_path - end - - local file, err = io.open(save_path, "w") + local file, err = io.open(cache_file_path, "w") if file then - file:write(vim.fn.json_encode(cache_notes)) + file:write(vim.json.encode(cache_notes)) file:close() else error(table.concat { "Couldn't write vault index to the file: ", save_path, ". Description: ", err }) @@ -59,7 +52,7 @@ local create_on_file_change_callback = function(self) local update_cache_file = function() vim.schedule(function() - save_cache_notes_to_file(cache_notes, self.client.opts.cache.path) + save_cache_notes_to_file(cache_notes, self:get_cache_path()) end) end @@ -72,7 +65,7 @@ local create_on_file_change_callback = function(self) local update_cache_dictionary = function(note) if note then local founded_cache = { - absolute_path = absolute_path, + absolute_path = file.absolute_path, aliases = note.aliases, last_updated = file.stat.mtime.sec, } @@ -119,7 +112,7 @@ local check_cache_notes_are_fresh = function(self) if completed == total then vim.schedule(function() - save_cache_notes_to_file(updated, self.client.opts.cache.path) + save_cache_notes_to_file(updated, self:get_cache_path()) end) end end @@ -183,7 +176,7 @@ end ---@param self obsidian.Cache local check_vault_cache = function(self) - check_file_exists(self.client.opts.cache.path, function(exists) + check_file_exists(self:get_cache_path(), function(exists) if exists then vim.schedule(function() check_cache_notes_are_fresh(self) @@ -201,7 +194,7 @@ Cache.new = function(client) local self = Cache.init() self.client = client - if client.opts.cache.enable then + if client.opts.cache.enabled then enable_filewatch(self) check_vault_cache(self) @@ -212,14 +205,15 @@ end --- Reads all notes in the vaults and returns the founded data. ---@param client obsidian.Client ----@return { [string]: obsidian.cache.CacheNote } -local get_cache_notes_from_vault = function(client) +---@param callback fun (note_caches: { [string]: obsidian.cache.CacheNote }) +local get_cache_notes_from_vault = function(client, callback) ---@type { [string]: obsidian.cache.CacheNote } local created_note_caches = {} - for notepath in search.find(client.dir, "", nil) do - --TODO make async - local note = Note.from_file(notepath, { read_only_frontmatter = true }) + local tx, rx = channel.oneshot() + + local on_find_match = function(path_match) + local note = Note.from_file(path_match, { read_only_frontmatter = true }) local absolute_path = note.path.filename local relative_path = absolute_path:gsub(client.dir.filename .. "/", "") @@ -244,37 +238,49 @@ local get_cache_notes_from_vault = function(client) created_note_caches[relative_path] = note_cache end - return created_note_caches + local on_exit = function(_) + tx() + end + + search.find_async(client.dir, "", nil, on_find_match, on_exit) + + async.run(function() + rx() + return created_note_caches + end, callback) end --- Reads all notes in the vaults and saves them to the cache file. ---@param self obsidian.Cache Cache.rebuild_cache = function(self) - if not self.client.opts.cache.enable then + if not self.client.opts.cache.enabled then log.error "The cache is disabled. Cannot index vault." + return; end - local founded_links = get_cache_notes_from_vault(self.client) - - save_cache_notes_to_file(founded_links, self.client.opts.cache.path) + log.info("Rebuilding cache...") - log.info "Vault was indexed succesfully." + get_cache_notes_from_vault(self.client, function(founded_links) + save_cache_notes_to_file(founded_links, self:get_cache_path()) + log.info("The cache was rebuild.") + end) end ---Reads the cache file from client.opts.cache.path and returns the loaded cache. ---@param self obsidian.Cache ---@return { [string]: obsidian.cache.CacheNote }|? Key is the relative path to the vault, value is the cache of the note. Cache.get_cache_notes_from_file = function(self) - local file, err = io.open(self.client.opts.cache.path, "r") + local file, err = io.open(self:get_cache_path(), "r") if file then local links_json = file:read() file:close() - return vim.fn.json_decode(links_json) - else + return vim.json.decode(links_json) + elseif err then log.err(err) - return nil end + + return nil end ---Reads the cache file from client.opts.cache.path and returns founded note cache without key. @@ -282,17 +288,13 @@ end ---@return obsidian.cache.CacheNote[] Cache.get_cache_notes_without_key = function(self) local cache_with_index = self:get_cache_notes_from_file() + assert(cache_with_index) + return vim.tbl_values(cache_with_index) +end - if not cache_with_index then - return {} - end - - local cache_without_index = {} - for _, value in pairs(cache_with_index) do - table.insert(cache_without_index, value) - end - - return cache_without_index +Cache.get_cache_path = function(self) + local normalized_path = vim.fs.normalize(self.client.opts.cache.path) + return vim.fs.joinpath(self.client.dir.filename, normalized_path) end return Cache diff --git a/lua/obsidian/commands/quick_switch.lua b/lua/obsidian/commands/quick_switch.lua index 6d1ca79e..2a8d2ff2 100644 --- a/lua/obsidian/commands/quick_switch.lua +++ b/lua/obsidian/commands/quick_switch.lua @@ -10,7 +10,7 @@ return function(client, data) return end - picker:find_notes { use_cache = client.opts.cache.enable } + picker:find_notes { use_cache = client.opts.cache.enabled } else client:resolve_note_async_with_picker_fallback(data.args, function(note) client:open_note(note) diff --git a/lua/obsidian/commands/index_vault.lua b/lua/obsidian/commands/rebuild_cache.lua similarity index 100% rename from lua/obsidian/commands/index_vault.lua rename to lua/obsidian/commands/rebuild_cache.lua diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index e25a9dc7..10f48cb7 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -582,15 +582,16 @@ end ---@class obsidian.config.CacheOpts --- ----@field enable boolean|? Use cache when searching for notes ----@field path string The file where the cache will be saved +---@field enabled boolean|? Use cache when searching for notes. +---@field path string The file where the cache will be saved. If the path should be absolute, it will be +---joined with the root of the vault. config.CacheOpts = {} ---@return obsidian.config.CacheOpts config.CacheOpts.default = function() return { - enable = false, - path = "./.cache.json", + enabled = false, + path = ".cache.json", } end From 593d6a1a3ed7a3239df27b12a1cfd70b72420f7d Mon Sep 17 00:00:00 2001 From: kostabekre Date: Wed, 25 Jun 2025 10:04:00 +0300 Subject: [PATCH 29/42] added tags, fixed bug with plenary (too many files), style fix --- lua/obsidian/api.lua | 44 +++++++++++++++++ lua/obsidian/cache.lua | 58 +++++++++++++--------- lua/obsidian/commands/init.lua | 38 +++++++-------- lua/obsidian/config.lua | 10 ++-- lua/obsidian/filewatch.lua | 34 ++++++------- lua/obsidian/os_util.lua | 46 ------------------ lua/obsidian/pickers/_telescope.lua | 75 +++++++++++++++-------------- lua/obsidian/search.lua | 4 +- 8 files changed, 161 insertions(+), 148 deletions(-) delete mode 100644 lua/obsidian/os_util.lua diff --git a/lua/obsidian/api.lua b/lua/obsidian/api.lua index 1bb3c469..0928e312 100644 --- a/lua/obsidian/api.lua +++ b/lua/obsidian/api.lua @@ -535,4 +535,48 @@ M.get_icon = function(path) return nil end +---Gets all notes from the vault. +---@param path string The path to the vault. +---@return string[]|? The path to the notes. +M.get_all_notes_from_vault = function(path) + local handle = io.popen("fd -t file -a --base-directory " .. path) + + if not handle then + log.err "Failed to execute command" + return nil + end + + local files = {} + + for file in handle:lines() do + table.insert(files, file) + end + + handle:close() + + return files +end + +---Gets all subfolders from the vault. +---@param path string The path to the vault. +---@return string[]|? The path to the subfolders +M.get_sub_dirs_from_vault = function(path) + local handle = io.popen("fd -t directory -a --base-directory " .. path) + + if not handle then + log.err "Failed to execute command" + return nil + end + + local subdirs = {} + + for dir in handle:lines() do + table.insert(subdirs, dir) + end + + handle:close() + + return subdirs +end + return M diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index 697cab68..1194c355 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -1,12 +1,10 @@ local async = require "plenary.async" local abc = require "obsidian.abc" -local search = require "obsidian.search" -local channel = require("plenary.async.control").channel local Note = require "obsidian.note" local log = require "obsidian.log" local EventTypes = require("obsidian.filewatch").EventTypes local uv = vim.uv -local os_util = require("obsidian.os_util") +local api = require "obsidian.api" ---This class allows you to find the notes in your vault more quickly. ---It scans your vault and saves the founded metadata to the file specified in your CacheOpts (by default it's ".cache.json"). @@ -23,6 +21,7 @@ local Cache = abc.new_class() ---@field absolute_path string The full path to the note. ---@field aliases string[] The alises of the note founded in the frontmatter. ---@field last_updated number The last time the note was updated in seconds since epoch. +---@field tags string[] The tags of the note. ---Converts the cache to JSON and saves to the file at the given path. ---@param cache_notes { [string]: obsidian.cache.CacheNote } Dictionary where key is the relative path and value is the cache of the note. @@ -34,7 +33,7 @@ local save_cache_notes_to_file = function(cache_notes, cache_file_path) file:write(vim.json.encode(cache_notes)) file:close() else - error(table.concat { "Couldn't write vault index to the file: ", save_path, ". Description: ", err }) + error(table.concat { "Couldn't write vault index to the file: ", cache_file_path, ". Description: ", err }) end end @@ -96,7 +95,7 @@ end ---Checks for note cache that were updated outside the vault. ---@param self obsidian.Cache local check_cache_notes_are_fresh = function(self) - local founded_notes = os_util.get_all_notes_from_vault(self.client:vault_root().filename) + local founded_notes = api.get_all_notes_from_vault(self.client:vault_root().filename) local cache_notes = self:get_cache_notes_from_file() if not cache_notes or not founded_notes then @@ -130,11 +129,14 @@ local check_cache_notes_are_fresh = function(self) local cache_note = cache_notes[relative_path] local aliases + local tags if cache_note and cache_note.last_updated == stat.mtime.sec then aliases = cache_note.aliases + tags = cache_note.tags else local note = Note.from_file(founded_note, { read_only_frontmatter = true }) aliases = note.aliases + tags = note.tags end ---@type obsidian.cache.CacheNote @@ -142,6 +144,7 @@ local check_cache_notes_are_fresh = function(self) absolute_path = founded_note, last_updated = stat.mtime.sec, aliases = aliases, + tags = tags, } updated[relative_path] = updated_cache @@ -151,7 +154,6 @@ local check_cache_notes_are_fresh = function(self) end end - ---Checks that file exits ---@param path string ---@param callback fun (result: boolean) @@ -164,7 +166,7 @@ end ---Watches the vault for changes. ---@param self obsidian.Cache local enable_filewatch = function(self) - local filewatch = require("obsidian.filewatch") + local filewatch = require "obsidian.filewatch" filewatch.watch(self.client.dir.filename, create_on_file_change_callback(self)) vim.api.nvim_create_autocmd({ "QuitPre", "ExitPre" }, { @@ -194,7 +196,7 @@ Cache.new = function(client) local self = Cache.init() self.client = client - if client.opts.cache.enabled then + if client.opts.cache.enable then enable_filewatch(self) check_vault_cache(self) @@ -210,11 +212,17 @@ local get_cache_notes_from_vault = function(client, callback) ---@type { [string]: obsidian.cache.CacheNote } local created_note_caches = {} - local tx, rx = channel.oneshot() + local founded_notes = api.get_all_notes_from_vault(client.dir.filename) + + assert(founded_notes) - local on_find_match = function(path_match) - local note = Note.from_file(path_match, { read_only_frontmatter = true }) + local notes_parsed = 0 + local on_exit = function() + callback(created_note_caches) + end + + local on_note_parsed = function(note) local absolute_path = note.path.filename local relative_path = absolute_path:gsub(client.dir.filename .. "/", "") @@ -233,36 +241,38 @@ local get_cache_notes_from_vault = function(client, callback) absolute_path = absolute_path, aliases = note.aliases, last_updated = last_updated, + tags = note.tags or {}, } created_note_caches[relative_path] = note_cache - end - local on_exit = function(_) - tx() - end + notes_parsed = notes_parsed + 1 - search.find_async(client.dir, "", nil, on_find_match, on_exit) + if notes_parsed == #founded_notes then + on_exit() + end + end - async.run(function() - rx() - return created_note_caches - end, callback) + for _, note_path in ipairs(founded_notes) do + async.run(function() + return Note.from_file_async(note_path, { read_only_frontmatter = true }) + end, on_note_parsed) + end end --- Reads all notes in the vaults and saves them to the cache file. ---@param self obsidian.Cache Cache.rebuild_cache = function(self) - if not self.client.opts.cache.enabled then + if not self.client.opts.cache.enable then log.error "The cache is disabled. Cannot index vault." - return; + return end - log.info("Rebuilding cache...") + log.info "Rebuilding cache..." get_cache_notes_from_vault(self.client, function(founded_links) save_cache_notes_to_file(founded_links, self:get_cache_path()) - log.info("The cache was rebuild.") + log.info "The cache was rebuild." end) end diff --git a/lua/obsidian/commands/init.lua b/lua/obsidian/commands/init.lua index 54e438ca..393850d6 100644 --- a/lua/obsidian/commands/init.lua +++ b/lua/obsidian/commands/init.lua @@ -15,25 +15,25 @@ end local function get_commands_by_context(commands, is_visual, is_note) local choices = vim.tbl_values(commands) return vim - .iter(choices) - :filter(function(config) - if is_visual then - return config.range ~= nil - else - return config.range == nil - end - end) - :filter(function(config) - if is_note then - return true - else - return not config.note_action - end - end) - :map(function(config) - return config.name - end) - :totable() + .iter(choices) + :filter(function(config) + if is_visual then + return config.range ~= nil + else + return config.range == nil + end + end) + :filter(function(config) + if is_note then + return true + else + return not config.note_action + end + end) + :map(function(config) + return config.name + end) + :totable() end local function show_menu(data) diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 62242c45..164a1def 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -425,8 +425,8 @@ config.normalize = function(opts, defaults) if warn then log.warn_once( "The config options 'completion.prepend_note_id', 'completion.prepend_note_path', and 'completion.use_path_only' " - .. "are deprecated. Please use 'wiki_link_func' instead.\n" - .. "See https://github.com/epwalsh/obsidian.nvim/pull/406" + .. "are deprecated. Please use 'wiki_link_func' instead.\n" + .. "See https://github.com/epwalsh/obsidian.nvim/pull/406" ) end end @@ -448,7 +448,7 @@ config.normalize = function(opts, defaults) opts.completion.preferred_link_style = nil log.warn_once( "The config option 'completion.preferred_link_style' is deprecated, please use the top-level " - .. "'preferred_link_style' instead." + .. "'preferred_link_style' instead." ) end @@ -457,7 +457,7 @@ config.normalize = function(opts, defaults) opts.completion.new_notes_location = nil log.warn_once( "The config option 'completion.new_notes_location' is deprecated, please use the top-level " - .. "'new_notes_location' instead." + .. "'new_notes_location' instead." ) end @@ -465,7 +465,7 @@ config.normalize = function(opts, defaults) opts.detect_cwd = nil log.warn_once( "The 'detect_cwd' field is deprecated and no longer has any affect.\n" - .. "See https://github.com/epwalsh/obsidian.nvim/pull/366 for more details." + .. "See https://github.com/epwalsh/obsidian.nvim/pull/366 for more details." ) end diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index 0feb3e72..1a38006b 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -1,6 +1,6 @@ local uv = vim.loop local util = require "obsidian.util" -local os_util = require("obsidian.os_util") +local api = require "obsidian.api" local M = {} @@ -76,8 +76,8 @@ local function watch_path(path, on_event, on_error, opts) end local flags = { - watch_entry = false, -- true = if you pass dir, watch the dir inode only, not the dir content - stat = false, -- true = don't use inotify/kqueue but periodic check, not implemented + watch_entry = false, -- true = if you pass dir, watch the dir inode only, not the dir content + stat = false, -- true = don't use inotify/kqueue but periodic check, not implemented recursive = opts.recursive, -- true = watch dirs inside dirs. For now only works on Windows and MacOS } @@ -89,7 +89,7 @@ local function watch_path(path, on_event, on_error, opts) local add_to_queue = function(send_arg) table.insert(queue_to_send, send_arg) - queue_timer:stop(); + queue_timer:stop() queue_timer:start(CALLBACK_AFTER_INTERVAL, 0, function() on_event(queue_to_send) @@ -113,11 +113,11 @@ local function watch_path(path, on_event, on_error, opts) uv.fs_stat(full_path, function(stat_err, stat) if stat_err then - on_event({ + on_event { absolute_path = full_path, event = M.EventTypes.deleted, - stat = nil - }) + stat = nil, + } return end @@ -130,11 +130,11 @@ local function watch_path(path, on_event, on_error, opts) event_type = M.EventTypes.unknown end - add_to_queue({ + add_to_queue { absolute_path = full_path, event = event_type, - stat = stat - }) + stat = stat, + } end) end @@ -159,9 +159,7 @@ M.watch = function(path, callback, on_error) error "Path cannot be empty." end - if not callback then - error "Callback cannot be empty!" - end + assert(callback) if on_error == nil then on_error = make_default_error_cb(path) @@ -169,9 +167,7 @@ M.watch = function(path, callback, on_error) local new_timer = uv.new_timer() - if not new_timer then - error("Couldn't create queue timer!") - end + assert(new_timer) queue_timer = new_timer @@ -181,7 +177,11 @@ M.watch = function(path, callback, on_error) if sysname == util.OSType.Linux then table.insert(watch_handlers, watch_path(path, callback, on_error, { recursive = false })) - for _, dir in ipairs(os_util.get_sub_dirs_from_vault(path)) do + local subfolders = api.get_sub_dirs_from_vault(path) + + assert(subfolders) + + for _, dir in ipairs(subfolders) do table.insert(watch_handlers, watch_path(dir, callback, on_error, { recursive = false })) end else diff --git a/lua/obsidian/os_util.lua b/lua/obsidian/os_util.lua deleted file mode 100644 index 538acf32..00000000 --- a/lua/obsidian/os_util.lua +++ /dev/null @@ -1,46 +0,0 @@ -local log = require "obsidian.log" - -local M = {} - ----Gets all notes from the vault. ----@param path string The path to the vault. ----@return string[]|? The path to the notes. -M.get_all_notes_from_vault = function(path) - local handle = io.popen("fd -t file -a --base-directory " .. path) - if not handle then - log.err "Failed to execute command" - return nil - end - - local files = {} - - for file in handle:lines() do - table.insert(files, file) - end - - handle:close() - - return files -end - ----Gets all subfolders from the vault. ----@param path string The path to the vault. ----@return string[] The path to the subfolders -M.get_sub_dirs_from_vault = function(path) - local handle = io.popen("fd -t directory -a --base-directory " .. path) - if not handle then - error "Failed to execute command" - end - - local subdirs = {} - - for dir in handle:lines() do - table.insert(subdirs, dir) - end - - handle:close() - - return subdirs -end - -return M diff --git a/lua/obsidian/pickers/_telescope.lua b/lua/obsidian/pickers/_telescope.lua index 6938708c..694176bc 100644 --- a/lua/obsidian/pickers/_telescope.lua +++ b/lua/obsidian/pickers/_telescope.lua @@ -175,6 +175,7 @@ local create_cache_picker = function(self, prompt_title, opts) ---@return obsidian.pickers.telescope_picker.CacheSelectedEntry entry_maker = function(entry) local concated_aliases = table.concat(entry.aliases, "|") + local concated_tags = table.concat(entry.tags, "#") local relative_path = entry.absolute_path:gsub(self.client.dir.filename .. "/", "") local display_name @@ -184,6 +185,10 @@ local create_cache_picker = function(self, prompt_title, opts) display_name = relative_path end + if concated_tags and concated_tags ~= "" then + display_name = table.concat({ display_name, concated_tags }, "#") + end + return { value = entry, display = display_name, @@ -311,7 +316,7 @@ TelescopePicker.pick = function(self, values, opts) local picker_conf = conf.pickers[picker_name] if picker_conf and picker_conf.theme then picker_opts = - vim.tbl_extend("force", picker_opts, require("telescope.themes")["get_" .. picker_conf.theme] {}) + vim.tbl_extend("force", picker_opts, require("telescope.themes")["get_" .. picker_conf.theme] {}) break end end @@ -321,42 +326,42 @@ TelescopePicker.pick = function(self, values, opts) local make_entry_from_string = make_entry.gen_from_string(picker_opts) pickers - .new(picker_opts, { - prompt_title = prompt_title, - finder = finders.new_table { - results = values, - entry_maker = function(v) - if type(v) == "string" then - return make_entry_from_string(v) - else - local ordinal = v.ordinal - if ordinal == nil then - ordinal = "" - if type(v.display) == "string" then - ordinal = ordinal .. v.display - end - if v.filename ~= nil then - ordinal = ordinal .. " " .. v.filename - end + .new(picker_opts, { + prompt_title = prompt_title, + finder = finders.new_table { + results = values, + entry_maker = function(v) + if type(v) == "string" then + return make_entry_from_string(v) + else + local ordinal = v.ordinal + if ordinal == nil then + ordinal = "" + if type(v.display) == "string" then + ordinal = ordinal .. v.display + end + if v.filename ~= nil then + ordinal = ordinal .. " " .. v.filename end - - return { - value = v.value, - display = displayer, - ordinal = ordinal, - filename = v.filename, - valid = v.valid, - lnum = v.lnum, - col = v.col, - raw = v, - } end - end, - }, - sorter = conf.values.generic_sorter(picker_opts), - previewer = previewer, - }) - :find() + + return { + value = v.value, + display = displayer, + ordinal = ordinal, + filename = v.filename, + valid = v.valid, + lnum = v.lnum, + col = v.col, + raw = v, + } + end + end, + }, + sorter = conf.values.generic_sorter(picker_opts), + previewer = previewer, + }) + :find() end return TelescopePicker diff --git a/lua/obsidian/search.lua b/lua/obsidian/search.lua index 85b361cd..ec1b3cc9 100644 --- a/lua/obsidian/search.lua +++ b/lua/obsidian/search.lua @@ -271,9 +271,9 @@ end ---@field ignore_case boolean|? ---@field smart_case boolean|? ---@field exclude string[]|? paths to exclude ----@field max_count_per_file integer|? +---@field max_count_per_file integer|? Limit the number of matching lines per file searched to max_count_per_file. ---@field escape_path boolean|? ----@field include_non_markdown boolean|? +---@field include_non_markdown boolean|? search for .*? If false, searches for .md local SearchOpts = {} M.SearchOpts = SearchOpts From 78023b551b2d74a50a6e758e7c6f5dbf2cc95437 Mon Sep 17 00:00:00 2001 From: neo451 <111681693+neo451@users.noreply.github.com> Date: Wed, 25 Jun 2025 23:33:25 +0800 Subject: [PATCH 30/42] Apply suggestions from code review --- lua/obsidian/pickers/_telescope.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/obsidian/pickers/_telescope.lua b/lua/obsidian/pickers/_telescope.lua index 694176bc..51ddbac9 100644 --- a/lua/obsidian/pickers/_telescope.lua +++ b/lua/obsidian/pickers/_telescope.lua @@ -135,7 +135,7 @@ end ---@param self obsidian.pickers.TelescopePicker ---@param prompt_title string ---@param opts obsidian.PickerFindOpts ----@return Picker +---@return obsidian.Picker local create_cache_picker = function(self, prompt_title, opts) local pickers = require "telescope.pickers" local finders = require "telescope.finders" From 27210820c8437a055a159001d366b521314e0186 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Wed, 25 Jun 2025 21:44:11 +0300 Subject: [PATCH 31/42] Code review changes --- README.md | 3 --- lua/obsidian/api.lua | 34 +++++++++-------------------- lua/obsidian/cache.lua | 6 ++--- lua/obsidian/config.lua | 4 ++-- lua/obsidian/pickers/_telescope.lua | 1 + 5 files changed, 16 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 24f7be66..cb2193f5 100644 --- a/README.md +++ b/README.md @@ -134,9 +134,6 @@ For remapping and creating your own mappings, see [Keymaps](https://github.com/o - **MacOS** users need [`pngpaste`](https://github.com/jcsalterego/pngpaste) (`brew install pngpaste`) for `:Obsidian paste_img`. - **Linux** users need xclip (X11) or wl-clipboard (Wayland) for `:Obsidian paste_img`. -- Optional dependencies: - - Backend: [fd](https://github.com/sharkdp/fd), see [fd#installation](https://github.com/sharkdp/fd#installation) for `:Obsidian rebuild_cache` - ### Plugin dependencies The only **required** plugin dependency is [plenary.nvim](https://github.com/nvim-lua/plenary.nvim), but there are a number of optional dependencies that enhance the obsidian.nvim experience. diff --git a/lua/obsidian/api.lua b/lua/obsidian/api.lua index 0928e312..a15af56e 100644 --- a/lua/obsidian/api.lua +++ b/lua/obsidian/api.lua @@ -539,21 +539,14 @@ end ---@param path string The path to the vault. ---@return string[]|? The path to the notes. M.get_all_notes_from_vault = function(path) - local handle = io.popen("fd -t file -a --base-directory " .. path) - - if not handle then - log.err "Failed to execute command" - return nil - end - local files = {} - for file in handle:lines() do - table.insert(files, file) + for name, t in vim.fs.dir(path, { depth = 10 }) do + if t == "file" and vim.endswith(name, ".md") then + local full_path = vim.fs.joinpath(path, name) + files[#files + 1] = full_path + end end - - handle:close() - return files end @@ -561,21 +554,14 @@ end ---@param path string The path to the vault. ---@return string[]|? The path to the subfolders M.get_sub_dirs_from_vault = function(path) - local handle = io.popen("fd -t directory -a --base-directory " .. path) - - if not handle then - log.err "Failed to execute command" - return nil - end - local subdirs = {} - for dir in handle:lines() do - table.insert(subdirs, dir) + for name, t in vim.fs.dir(path, { depth = 10 }) do + if t == "directory" then + local full_path = vim.fs.joinpath(path, name) + subdirs[#subdirs + 1] = full_path + end end - - handle:close() - return subdirs end diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index 1194c355..a98636a5 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -196,7 +196,7 @@ Cache.new = function(client) local self = Cache.init() self.client = client - if client.opts.cache.enable then + if client.opts.cache.enabled then enable_filewatch(self) check_vault_cache(self) @@ -263,8 +263,8 @@ end --- Reads all notes in the vaults and saves them to the cache file. ---@param self obsidian.Cache Cache.rebuild_cache = function(self) - if not self.client.opts.cache.enable then - log.error "The cache is disabled. Cannot index vault." + if not self.client.opts.cache.enabled then + log.error "The cache is disabled. Cannot rebuild cache." return end diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 164a1def..2f7f22d4 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -312,10 +312,10 @@ config.default = { ---@class obsidian.config.CacheOpts --- - ---@field enable boolean|? Use cache when searching for notes + ---@field enabled boolean|? Use cache when searching for notes ---@field path string The file where the cache will be saved cache = { - enable = false, + enabled = false, path = "./.cache.json", }, diff --git a/lua/obsidian/pickers/_telescope.lua b/lua/obsidian/pickers/_telescope.lua index 51ddbac9..48d8c5d2 100644 --- a/lua/obsidian/pickers/_telescope.lua +++ b/lua/obsidian/pickers/_telescope.lua @@ -212,6 +212,7 @@ TelescopePicker.find_files = function(self, opts) } if opts.use_cache then + ---@diagnostic disable-next-line: undefined-field create_cache_picker(self, prompt_title, opts):find() else telescope.find_files { From 151d7926aa442859f3f3681ec10b01885dd04e12 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Wed, 25 Jun 2025 21:58:25 +0300 Subject: [PATCH 32/42] fix update errors --- lua/obsidian/cache.lua | 2 ++ lua/obsidian/filewatch.lua | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index a98636a5..be173dd5 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -63,10 +63,12 @@ local create_on_file_change_callback = function(self) ---@param note obsidian.Note|? local update_cache_dictionary = function(note) if note then + ---@type obsidian.cache.CacheNote local founded_cache = { absolute_path = file.absolute_path, aliases = note.aliases, last_updated = file.stat.mtime.sec, + tags = note.tags, } cache_notes[relative_path] = founded_cache diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index 1a38006b..75c83a69 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -109,7 +109,7 @@ local function watch_path(path, on_event, on_error, opts) local folder_path = uv.fs_event_getpath(handle) - local full_path = table.concat { folder_path, filename } + local full_path = table.concat { folder_path, "/", filename } uv.fs_stat(full_path, function(stat_err, stat) if stat_err then From 2aec26990fc4cc6ff81113b208b6c4cd7046a63c Mon Sep 17 00:00:00 2001 From: kostabekre Date: Thu, 26 Jun 2025 09:55:30 +0300 Subject: [PATCH 33/42] used joinpath for cross platform, added an option to show or hide tags in the quick switch --- README.md | 1 + lua/obsidian/config.lua | 2 ++ lua/obsidian/filewatch.lua | 2 +- lua/obsidian/pickers/_telescope.lua | 6 +++--- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cb2193f5..da7f44b0 100644 --- a/README.md +++ b/README.md @@ -602,6 +602,7 @@ require("obsidian").setup { cache = { enabled = false, path = "./.cache.json", + show_tags = false -- will show tags after a note name and after aliases. } } ``` diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 2f7f22d4..d1282313 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -314,9 +314,11 @@ config.default = { --- ---@field enabled boolean|? Use cache when searching for notes ---@field path string The file where the cache will be saved + ---@field show_tags boolean|? Show tags in the picker cache = { enabled = false, path = "./.cache.json", + show_tags = false, }, ---@class obsidian.config.StatuslineOpts diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index 75c83a69..761cdcde 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -109,7 +109,7 @@ local function watch_path(path, on_event, on_error, opts) local folder_path = uv.fs_event_getpath(handle) - local full_path = table.concat { folder_path, "/", filename } + local full_path = vim.fs.joinpath(folder_path, filename) uv.fs_stat(full_path, function(stat_err, stat) if stat_err then diff --git a/lua/obsidian/pickers/_telescope.lua b/lua/obsidian/pickers/_telescope.lua index 48d8c5d2..75e75b91 100644 --- a/lua/obsidian/pickers/_telescope.lua +++ b/lua/obsidian/pickers/_telescope.lua @@ -175,7 +175,7 @@ local create_cache_picker = function(self, prompt_title, opts) ---@return obsidian.pickers.telescope_picker.CacheSelectedEntry entry_maker = function(entry) local concated_aliases = table.concat(entry.aliases, "|") - local concated_tags = table.concat(entry.tags, "#") + local concated_tags = table.concat(entry.tags, " #") local relative_path = entry.absolute_path:gsub(self.client.dir.filename .. "/", "") local display_name @@ -185,8 +185,8 @@ local create_cache_picker = function(self, prompt_title, opts) display_name = relative_path end - if concated_tags and concated_tags ~= "" then - display_name = table.concat({ display_name, concated_tags }, "#") + if self.client.opts.cache.show_tags and concated_tags and concated_tags ~= "" then + display_name = table.concat({ display_name, concated_tags }, " #") end return { From e7cdbd21ef2818e712069ffd010cdf18daf9c475 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Sun, 6 Jul 2025 18:23:33 +0300 Subject: [PATCH 34/42] fix: dropped abc in cache, cache doesn't use client --- lua/obsidian/cache.lua | 148 +++++++++++------------- lua/obsidian/commands/rebuild_cache.lua | 4 +- lua/obsidian/filewatch.lua | 11 ++ lua/obsidian/init.lua | 1 + lua/obsidian/pickers/_telescope.lua | 12 +- lua/obsidian/workspace.lua | 4 + 6 files changed, 95 insertions(+), 85 deletions(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index be173dd5..50403ae7 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -1,19 +1,15 @@ local async = require "plenary.async" -local abc = require "obsidian.abc" local Note = require "obsidian.note" local log = require "obsidian.log" local EventTypes = require("obsidian.filewatch").EventTypes local uv = vim.uv local api = require "obsidian.api" ----This class allows you to find the notes in your vault more quickly. +---This table allows you to find the notes in your vault more quickly. ---It scans your vault and saves the founded metadata to the file specified in your CacheOpts (by default it's ".cache.json"). ---For now, this allows to search for alises of your ---notes in much shorter time. ----@class obsidian.Cache : obsidian.ABC ---- ----@field client obsidian.Client -local Cache = abc.new_class() +local M = {} ---Contains some information from the metadata of your note plus additional info. ---@class obsidian.cache.CacheNote @@ -37,13 +33,27 @@ local save_cache_notes_to_file = function(cache_notes, cache_file_path) end end ----Update the cache file for changed files. ----@param self obsidian.Cache +local get_cache_path = function() + local workspace_path = Obsidian.dir.filename + local opts = Obsidian.opts + local normalized_path = vim.fs.normalize(opts.cache.path) + return vim.fs.joinpath(workspace_path, normalized_path) +end + +---@param cache_notes { [string]: obsidian.cache.CacheNote } Dictionary where key is the relative path and value is the cache of the note. +local update_cache = function(cache_notes) + Obsidian.cache = cache_notes + save_cache_notes_to_file(cache_notes, get_cache_path()) +end + +---Creates a funciton, which updates the cache file when notes in workspace are changed. ---@return fun (changed_files: obsidian.filewatch.CallbackArgs[]) -local create_on_file_change_callback = function(self) +local create_on_file_change_callback = function() return function(changed_files) + local workspace_path = Obsidian.dir.filename + vim.schedule(function() - local cache_notes = self:get_cache_notes_from_file() + local cache_notes = Obsidian.cache if not cache_notes then return @@ -51,14 +61,14 @@ local create_on_file_change_callback = function(self) local update_cache_file = function() vim.schedule(function() - save_cache_notes_to_file(cache_notes, self:get_cache_path()) + update_cache(cache_notes) end) end local left = #changed_files for _, file in ipairs(changed_files) do - local relative_path = file.absolute_path:gsub(self.client.dir.filename .. "/", "") + local relative_path = file.absolute_path:gsub(workspace_path .. "/", "") ---@param note obsidian.Note|? local update_cache_dictionary = function(note) @@ -94,13 +104,30 @@ local create_on_file_change_callback = function(self) end end +---Reads the cache file from client.opts.cache.path and returns the loaded cache. +---@return { [string]: obsidian.cache.CacheNote }|? Key is the relative path to the vault, value is the cache of the note. +local get_cache_notes_from_file = function() + local file, err = io.open(get_cache_path(), "r") + + if file then + local links_json = file:read() + file:close() + return vim.json.decode(links_json) + elseif err then + log.err(err) + end + + return nil +end + ---Checks for note cache that were updated outside the vault. ----@param self obsidian.Cache -local check_cache_notes_are_fresh = function(self) - local founded_notes = api.get_all_notes_from_vault(self.client:vault_root().filename) - local cache_notes = self:get_cache_notes_from_file() +local check_cache_notes_are_fresh = function() + local workspace_path = Obsidian.dir.filename - if not cache_notes or not founded_notes then + local founded_notes = api.get_all_notes_from_vault(workspace_path) + local old_cache_notes = get_cache_notes_from_file() + + if not old_cache_notes or not founded_notes then return end @@ -113,13 +140,13 @@ local check_cache_notes_are_fresh = function(self) if completed == total then vim.schedule(function() - save_cache_notes_to_file(updated, self:get_cache_path()) + update_cache(updated) end) end end for _, founded_note in ipairs(founded_notes) do - local relative_path = founded_note:gsub(self.client.dir.filename .. "/", "") + local relative_path = founded_note:gsub(workspace_path .. "/", "") uv.fs_stat(founded_note, function(err, stat) if err then @@ -128,7 +155,7 @@ local check_cache_notes_are_fresh = function(self) return end - local cache_note = cache_notes[relative_path] + local cache_note = old_cache_notes[relative_path] local aliases local tags @@ -166,10 +193,11 @@ local check_file_exists = function(path, callback) end ---Watches the vault for changes. ----@param self obsidian.Cache -local enable_filewatch = function(self) +local enable_filewatch = function() + local workspace_path = Obsidian.dir.filename + local filewatch = require "obsidian.filewatch" - filewatch.watch(self.client.dir.filename, create_on_file_change_callback(self)) + filewatch.watch(workspace_path, create_on_file_change_callback()) vim.api.nvim_create_autocmd({ "QuitPre", "ExitPre" }, { callback = function() @@ -178,43 +206,29 @@ local enable_filewatch = function(self) }) end ----@param self obsidian.Cache -local check_vault_cache = function(self) - check_file_exists(self:get_cache_path(), function(exists) +local check_vault_cache = function() + check_file_exists(get_cache_path(), function(exists) if exists then vim.schedule(function() - check_cache_notes_are_fresh(self) + check_cache_notes_are_fresh() end) else vim.schedule(function() - self:rebuild_cache() + M.rebuild_cache() end) end end) end ----@param client obsidian.Client -Cache.new = function(client) - local self = Cache.init() - self.client = client - - if client.opts.cache.enabled then - enable_filewatch(self) - - check_vault_cache(self) - end - - return self -end - --- Reads all notes in the vaults and returns the founded data. ----@param client obsidian.Client ---@param callback fun (note_caches: { [string]: obsidian.cache.CacheNote }) -local get_cache_notes_from_vault = function(client, callback) +local get_cache_notes_from_vault = function(callback) + local workspace_path = Obsidian.dir.filename + ---@type { [string]: obsidian.cache.CacheNote } local created_note_caches = {} - local founded_notes = api.get_all_notes_from_vault(client.dir.filename) + local founded_notes = api.get_all_notes_from_vault(workspace_path) assert(founded_notes) @@ -226,7 +240,7 @@ local get_cache_notes_from_vault = function(client, callback) local on_note_parsed = function(note) local absolute_path = note.path.filename - local relative_path = absolute_path:gsub(client.dir.filename .. "/", "") + local relative_path = absolute_path:gsub(workspace_path .. "/", "") local file_stat = uv.fs_stat(absolute_path) local last_updated @@ -263,50 +277,24 @@ local get_cache_notes_from_vault = function(client, callback) end --- Reads all notes in the vaults and saves them to the cache file. ----@param self obsidian.Cache -Cache.rebuild_cache = function(self) - if not self.client.opts.cache.enabled then +M.rebuild_cache = function() + if not Obsidian.opts.cache.enabled then log.error "The cache is disabled. Cannot rebuild cache." return end log.info "Rebuilding cache..." - get_cache_notes_from_vault(self.client, function(founded_links) - save_cache_notes_to_file(founded_links, self:get_cache_path()) + get_cache_notes_from_vault(function(cache_notes) + update_cache(cache_notes) log.info "The cache was rebuild." end) end ----Reads the cache file from client.opts.cache.path and returns the loaded cache. ----@param self obsidian.Cache ----@return { [string]: obsidian.cache.CacheNote }|? Key is the relative path to the vault, value is the cache of the note. -Cache.get_cache_notes_from_file = function(self) - local file, err = io.open(self:get_cache_path(), "r") - - if file then - local links_json = file:read() - file:close() - return vim.json.decode(links_json) - elseif err then - log.err(err) - end - - return nil -end - ----Reads the cache file from client.opts.cache.path and returns founded note cache without key. ----@param self obsidian.Cache ----@return obsidian.cache.CacheNote[] -Cache.get_cache_notes_without_key = function(self) - local cache_with_index = self:get_cache_notes_from_file() - assert(cache_with_index) - return vim.tbl_values(cache_with_index) -end +M.activate_cache = function() + enable_filewatch() -Cache.get_cache_path = function(self) - local normalized_path = vim.fs.normalize(self.client.opts.cache.path) - return vim.fs.joinpath(self.client.dir.filename, normalized_path) + check_vault_cache() end -return Cache +return M diff --git a/lua/obsidian/commands/rebuild_cache.lua b/lua/obsidian/commands/rebuild_cache.lua index 78059ffa..93ac8c92 100644 --- a/lua/obsidian/commands/rebuild_cache.lua +++ b/lua/obsidian/commands/rebuild_cache.lua @@ -1,5 +1,7 @@ +local cache = require "obsidian.cache" + ---@param client obsidian.Client ---@param data CommandArgs return function(client, data) - client.cache:rebuild_cache() + cache:rebuild_cache() end diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index 761cdcde..c0fa518d 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -161,6 +161,15 @@ M.watch = function(path, callback, on_error) assert(callback) + for _, handler in ipairs(watch_handlers) do + local handlerPath = handler:getpath() + + if handlerPath and path == handlerPath then + error("a file watch handler is already created for the given path - " .. path) + return + end + end + if on_error == nil then on_error = make_default_error_cb(path) end @@ -199,6 +208,8 @@ M.release_resources = function() end end + watch_handlers = {} + queue_timer:stop() if not queue_timer.is_closing then queue_timer:close() diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 6be116c1..58709145 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -82,6 +82,7 @@ obsidian.setup = function(opts) ---@field workspace obsidian.Workspace The current workspace. ---@field dir obsidian.Path The root of the vault for the current workspace. ---@field buf_dir obsidian.Path|? The parent directory of the current buffer. + ---@field cache { [string]: obsidian.cache.CacheNote } The cached notes to use ---@field opts obsidian.config.ClientOpts current options ---@field _opts obsidian.config.ClientOpts default options _G.Obsidian = {} -- init a state table diff --git a/lua/obsidian/pickers/_telescope.lua b/lua/obsidian/pickers/_telescope.lua index dd80ca9b..9674df7f 100644 --- a/lua/obsidian/pickers/_telescope.lua +++ b/lua/obsidian/pickers/_telescope.lua @@ -2,6 +2,7 @@ local telescope = require "telescope.builtin" local telescope_actions = require "telescope.actions" local actions_state = require "telescope.actions.state" +local api = require "obsidian.api" local Path = require "obsidian.path" local abc = require "obsidian.abc" local Picker = require "obsidian.pickers.picker" @@ -153,7 +154,8 @@ local create_cache_picker = function(self, prompt_title, opts) end vim.schedule(function() - self.client:open_note(selection.absolute_path) + local open_cmd = api.get_open_strategy(Obsidian.opts.open_notes_in) + api.open_buffer(selection.absolute_path, { cmd = open_cmd }) end) end) @@ -167,16 +169,18 @@ local create_cache_picker = function(self, prompt_title, opts) end, } + local cache_without_relative_path = vim.tbl_values(Obsidian.cache) + local workspace_path = Obsidian.dir.filename return pickers.new(picker_opts, { cwd = opts.dir, finder = finders.new_table { - results = self.client.cache:get_cache_notes_without_key(), + results = cache_without_relative_path, ---@param entry obsidian.cache.CacheNote ---@return obsidian.pickers.telescope_picker.CacheSelectedEntry entry_maker = function(entry) local concated_aliases = table.concat(entry.aliases, "|") local concated_tags = table.concat(entry.tags, " #") - local relative_path = entry.absolute_path:gsub(self.client.dir.filename .. "/", "") + local relative_path = entry.absolute_path:gsub(workspace_path .. "/", "") local display_name if concated_aliases and concated_aliases ~= "" then @@ -185,7 +189,7 @@ local create_cache_picker = function(self, prompt_title, opts) display_name = relative_path end - if self.client.opts.cache.show_tags and concated_tags and concated_tags ~= "" then + if Obsidian.opts.cache.show_tags and concated_tags and concated_tags ~= "" then display_name = table.concat({ display_name, concated_tags }, " #") end diff --git a/lua/obsidian/workspace.lua b/lua/obsidian/workspace.lua index 1cfc74e8..5522ed3a 100644 --- a/lua/obsidian/workspace.lua +++ b/lua/obsidian/workspace.lua @@ -250,6 +250,10 @@ Workspace.set = function(workspace, opts) pattern = "ObsidianWorkpspaceSet", data = { workspace = workspace }, }) + + if options.cache.enabled then + require("obsidian.cache").activate_cache() + end end return Workspace From 53d167a765f712755d46ae861543c6e37c5c0871 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Sun, 6 Jul 2025 18:28:33 +0300 Subject: [PATCH 35/42] fix: forgot to use Obsidian.opts in quick_switch --- lua/obsidian/commands/quick_switch.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/obsidian/commands/quick_switch.lua b/lua/obsidian/commands/quick_switch.lua index 0ab710bc..38d4c3e7 100644 --- a/lua/obsidian/commands/quick_switch.lua +++ b/lua/obsidian/commands/quick_switch.lua @@ -10,7 +10,7 @@ return function(client, data) return end - picker:find_notes { use_cache = client.opts.cache.enabled } + picker:find_notes { use_cache = Obsidian.opts.cache.enabled } else client:resolve_note_async_with_picker_fallback(data.args, function(note) client:open_note(note) From 0233ee6d3fdd1d29d9b40e56eb626f949339a4bc Mon Sep 17 00:00:00 2001 From: kostabekre Date: Wed, 16 Jul 2025 09:16:58 +0300 Subject: [PATCH 36/42] A lock file is created for a workspace, when enabling filewatch --- lua/obsidian/cache.lua | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index 50403ae7..26937529 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -196,12 +196,29 @@ end local enable_filewatch = function() local workspace_path = Obsidian.dir.filename + -- We need a lock file to check if a neovim instance is open in the workspace. + -- This prevents creating more filewatches in the same workspace which can lead to bugs and can decrease performaance. + local lock_name = table.concat { ".", Obsidian.dir.stem, ".lock" } + local lock_file_path = vim.fs.joinpath(workspace_path, lock_name) + + if uv.fs_stat(lock_file_path) then + return + end + + local lock_file_handler = io.open(lock_file_path, "w") + + assert(lock_file_handler) + + lock_file_handler:write() + lock_file_handler:close() + local filewatch = require "obsidian.filewatch" filewatch.watch(workspace_path, create_on_file_change_callback()) vim.api.nvim_create_autocmd({ "QuitPre", "ExitPre" }, { callback = function() filewatch.release_resources() + os.remove(lock_file_path) end, }) end From d4d193167947ba33a65a1e589fd9a7ba2e28c829 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Sun, 20 Jul 2025 09:50:58 +0300 Subject: [PATCH 37/42] .gitignore or .ignore is respected in filewatch and cache --- lua/obsidian/api.lua | 106 ++++++++++++++++++++++++++++++++++--- lua/obsidian/filewatch.lua | 5 +- 2 files changed, 100 insertions(+), 11 deletions(-) diff --git a/lua/obsidian/api.lua b/lua/obsidian/api.lua index ad90df5a..71dd0bb7 100644 --- a/lua/obsidian/api.lua +++ b/lua/obsidian/api.lua @@ -647,27 +647,117 @@ end M.get_all_notes_from_vault = function(path) local files = {} - for name, t in vim.fs.dir(path, { depth = 10 }) do - if t == "file" and vim.endswith(name, ".md") then - local full_path = vim.fs.joinpath(path, name) - files[#files + 1] = full_path + local cmd + local find_command + + -- checking for executable which support .ignore or .gitignore + if 1 == vim.fn.executable "rg" then + find_command = { "rg", "--files", "--color", "never", "--glob", "'*.md'", path } + cmd = "rg" + elseif 1 == vim.fn.executable "fd" then + find_command = { "fd", "--type", "f", "--color", "never", "--extension", "md", "--base-directory", path } + cmd = "fd" + elseif 1 == vim.fn.executable "fdfind" then + find_command = { "fdfind", "--type", "f", "--color", "never", "--extension", "md", "--base-directory", path } + cmd = "fdfind" + end + + if cmd then + local handle = io.popen(table.concat(find_command, " ")) + + if not handle then + log.error("couldn't execute cmd " .. cmd) + return {} + end + + for file in handle:lines() do + files[#files + 1] = file + end + else + -- If we couldn't find one of the executable, fallback to default implemendation + -- which doesn't respect .ignore or .gitignore. + for name, t in vim.fs.dir(path, { depth = 10 }) do + if t == "file" and vim.endswith(name, ".md") then + local full_path = vim.fs.joinpath(path, name) + files[#files + 1] = full_path + end end end + return files end +---Use greedy search to get the folder of the file. +---@param file_path string +---@param sep string +---@return string +local get_directory_path = function(file_path, sep) + return file_path:match("(.*" .. sep .. ")") +end + ---Gets all subfolders from the vault. ---@param path string The path to the vault. ---@return string[]|? The path to the subfolders M.get_sub_dirs_from_vault = function(path) local subdirs = {} - for name, t in vim.fs.dir(path, { depth = 10 }) do - if t == "directory" then - local full_path = vim.fs.joinpath(path, name) - subdirs[#subdirs + 1] = full_path + local cmd + local find_command + + -- checking for executable which support .ignore or .gitignore + if 1 == vim.fn.executable "fd" then + find_command = { "fd", "--type", "d", "--color", "never", "--base-directory", path } + cmd = "fd" + elseif 1 == vim.fn.executable "fdfind" then + find_command = { "fdfind", "--type", "f", "--color", "never", "--base-directory", path } + cmd = "fdfind" + elseif 1 == vim.fn.executable "rg" then + -- rg doesn't support searching folders, so we will filter output manually. + -- Unfortunatly, this won't return folders which are empty. + find_command = { "rg", "--files", "--color", "never", "--glob", "'*.md'", path } + cmd = "rg" + end + + if cmd then + local handle = io.popen(table.concat(find_command, " ")) + + if not handle then + log.error("couldn't execute cmd " .. cmd) + return {} + end + + if cmd == "rg" then + local path_separator = "/" + + local sysname = M.get_os() + + if sysname == M.OSType.Windows then + path_separator = "\\" + end + + for file in handle:lines() do + local subdir = get_directory_path(file, path_separator) + + if not vim.tbl_contains(subdirs, subdir) then + subdirs[#subdirs + 1] = subdir + end + end + else + for subdir in handle:lines() do + subdirs[#subdirs + 1] = subdir + end + end + else + -- the user doesn't have tools which support .gitignore or .ignore, + -- fallback to getting all folders + for name, t in vim.fs.dir(path, { depth = 10 }) do + if t == "directory" then + local full_path = vim.fs.joinpath(path, name) + subdirs[#subdirs + 1] = full_path + end end end + return subdirs end diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index c0fa518d..31f9b8db 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -1,5 +1,4 @@ local uv = vim.loop -local util = require "obsidian.util" local api = require "obsidian.api" local M = {} @@ -180,10 +179,10 @@ M.watch = function(path, callback, on_error) queue_timer = new_timer - local sysname = util.get_os() + local sysname = api.get_os() -- uv doesn't support recursive flag on Linux - if sysname == util.OSType.Linux then + if sysname == api.OSType.Linux then table.insert(watch_handlers, watch_path(path, callback, on_error, { recursive = false })) local subfolders = api.get_sub_dirs_from_vault(path) From 6dd1a7b07ec60833aea0ec09bb795fec34addde3 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Sun, 20 Jul 2025 09:51:44 +0300 Subject: [PATCH 38/42] lock file fix: use pid to check if the neovim isntance, which created the lock file exists. --- lua/obsidian/api.lua | 26 ++++++++++++++++++++++++++ lua/obsidian/cache.lua | 14 ++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lua/obsidian/api.lua b/lua/obsidian/api.lua index 71dd0bb7..aaaf8843 100644 --- a/lua/obsidian/api.lua +++ b/lua/obsidian/api.lua @@ -761,6 +761,32 @@ M.get_sub_dirs_from_vault = function(path) return subdirs end +---Cross-platform check if the pid exists. +---@param pid string +---@return boolean +M.check_pid_exists = function(pid) + local system = M.get_os() + + if system == M.OSType.Windows then + local cmd = string.format('tasklist /FI "PID eq %d" /NH', pid) + local pipe = io.popen(cmd) + assert(pipe) + local output = pipe:read() + pipe:close() + + return output:match(pid) ~= nil + else + local cmd = "kill -0 " .. pid + local ok = os.execute(cmd) + + if ok then + return true + else + return false + end + end +end + --- Resolve a basename to full path inside the vault. --- ---@param src string diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index 26937529..c07f0ede 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -202,14 +202,24 @@ local enable_filewatch = function() local lock_file_path = vim.fs.joinpath(workspace_path, lock_name) if uv.fs_stat(lock_file_path) then - return + local lock_file = io.open(lock_file_path, "r") + + assert(lock_file) + + local pid = lock_file:read() + lock_file:close() + + if api.check_pid_exists(pid) then + return + end end local lock_file_handler = io.open(lock_file_path, "w") assert(lock_file_handler) - lock_file_handler:write() + local current_nvim_pid = uv.os_getpid() + lock_file_handler:write(current_nvim_pid) lock_file_handler:close() local filewatch = require "obsidian.filewatch" From 3a6ce8026fdf9e267d8133529ff7e15809785e29 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Mon, 1 Sep 2025 13:03:25 +0300 Subject: [PATCH 39/42] moved lock file to the state folder, set the workspace based on the cwd, update variable names in filewatch --- lua/obsidian/cache.lua | 4 ++-- lua/obsidian/filewatch.lua | 26 +++++++++++++------------- lua/obsidian/init.lua | 15 ++++++++++++++- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index c07f0ede..d1bdaafd 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -198,8 +198,8 @@ local enable_filewatch = function() -- We need a lock file to check if a neovim instance is open in the workspace. -- This prevents creating more filewatches in the same workspace which can lead to bugs and can decrease performaance. - local lock_name = table.concat { ".", Obsidian.dir.stem, ".lock" } - local lock_file_path = vim.fs.joinpath(workspace_path, lock_name) + local lock_name = table.concat { Obsidian.dir.stem, ".lock" } + local lock_file_path = vim.fs.joinpath(vim.fn.stdpath "state", lock_name) if uv.fs_stat(lock_file_path) then local lock_file = io.open(lock_file_path, "r") diff --git a/lua/obsidian/filewatch.lua b/lua/obsidian/filewatch.lua index 31f9b8db..0a0b021b 100644 --- a/lua/obsidian/filewatch.lua +++ b/lua/obsidian/filewatch.lua @@ -137,10 +137,10 @@ local function watch_path(path, on_event, on_error, opts) end) end - local success, err, err_name = uv.fs_event_start(handle, path, flags, event_cb) + local success, err = uv.fs_event_start(handle, path, flags, event_cb) if not success then - error("couldn't create fs event! error - " .. err .. " err_name: " .. err_name) + error("couldn't create fs event! error - " .. err .. ". Path - " .. path) end return handle @@ -150,11 +150,11 @@ end ---Calls the callback function only after CALLBACK_AFTER_INTERVAL. ---If an error occured, called on_error function. ---TODO if a new folder will be created, it won't be tracked. ----@param path string The path to the watch folder. +---@param folder_path string The path to the watch folder. ---@param callback fun (changed_files: obsidian.filewatch.CallbackArgs[]) ---@param on_error fun (err: string)|? -M.watch = function(path, callback, on_error) - if not path or path == "" then +M.watch = function(folder_path, callback, on_error) + if not folder_path or folder_path == "" then error "Path cannot be empty." end @@ -163,14 +163,14 @@ M.watch = function(path, callback, on_error) for _, handler in ipairs(watch_handlers) do local handlerPath = handler:getpath() - if handlerPath and path == handlerPath then - error("a file watch handler is already created for the given path - " .. path) + if handlerPath and folder_path == handlerPath then + error("a file watch handler is already created for the given path - " .. folder_path) return end end if on_error == nil then - on_error = make_default_error_cb(path) + on_error = make_default_error_cb(folder_path) end local new_timer = uv.new_timer() @@ -183,17 +183,17 @@ M.watch = function(path, callback, on_error) -- uv doesn't support recursive flag on Linux if sysname == api.OSType.Linux then - table.insert(watch_handlers, watch_path(path, callback, on_error, { recursive = false })) + table.insert(watch_handlers, watch_path(folder_path, callback, on_error, { recursive = false })) - local subfolders = api.get_sub_dirs_from_vault(path) + local subfolders = api.get_sub_dirs_from_vault(folder_path) assert(subfolders) - for _, dir in ipairs(subfolders) do - table.insert(watch_handlers, watch_path(dir, callback, on_error, { recursive = false })) + for _, subfolder in ipairs(subfolders) do + table.insert(watch_handlers, watch_path(subfolder, callback, on_error, { recursive = false })) end else - watch_handlers = { watch_path(path, callback, on_error, { recursive = true }) } + watch_handlers = { watch_path(folder_path, callback, on_error, { recursive = true }) } end end diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index d8679570..8853cfe3 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -60,7 +60,20 @@ obsidian.setup = function(opts) Obsidian._opts = opts - obsidian.Workspace.set(Obsidian.workspaces[1]) + local cwd_path = vim.uv.cwd() + + assert(cwd_path) + + local selected_workspace = Obsidian.workspaces[1] + + for _, value in ipairs(Obsidian.workspaces) do + if value.path.filename == cwd_path then + selected_workspace = value + break + end + end + + obsidian.Workspace.set(selected_workspace) log.set_level(Obsidian.opts.log_level) From 89dbe34820ebd21388dacc8bca97e8966a10e616 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Mon, 1 Sep 2025 13:27:13 +0300 Subject: [PATCH 40/42] moved lock file from state to state/obsidian --- lua/obsidian/cache.lua | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index d1bdaafd..021a35ae 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -4,6 +4,7 @@ local log = require "obsidian.log" local EventTypes = require("obsidian.filewatch").EventTypes local uv = vim.uv local api = require "obsidian.api" +local util = require "obsidian.util" ---This table allows you to find the notes in your vault more quickly. ---It scans your vault and saves the founded metadata to the file specified in your CacheOpts (by default it's ".cache.json"). @@ -199,7 +200,20 @@ local enable_filewatch = function() -- We need a lock file to check if a neovim instance is open in the workspace. -- This prevents creating more filewatches in the same workspace which can lead to bugs and can decrease performaance. local lock_name = table.concat { Obsidian.dir.stem, ".lock" } - local lock_file_path = vim.fs.joinpath(vim.fn.stdpath "state", lock_name) + local obsidian_state_path = vim.fs.joinpath(vim.fn.stdpath "state", "obsidian/") + + local stat_of_state_folder = uv.fs_stat(obsidian_state_path) + + if not stat_of_state_folder then + local ok = uv.fs_mkdir(obsidian_state_path, assert(tonumber(755, 8))) + + if not ok then + log.err("Couldn't create folder at " .. obsidian_state_path) + return + end + end + + local lock_file_path = vim.fs.joinpath(obsidian_state_path, lock_name) if uv.fs_stat(lock_file_path) then local lock_file = io.open(lock_file_path, "r") @@ -214,13 +228,8 @@ local enable_filewatch = function() end end - local lock_file_handler = io.open(lock_file_path, "w") - - assert(lock_file_handler) - local current_nvim_pid = uv.os_getpid() - lock_file_handler:write(current_nvim_pid) - lock_file_handler:close() + util.write_file(lock_file_path, tostring(current_nvim_pid)) local filewatch = require "obsidian.filewatch" filewatch.watch(workspace_path, create_on_file_change_callback()) From a46c8483accdca0699b0a1bcc7b1c8ff6a366206 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Mon, 1 Sep 2025 15:06:41 +0300 Subject: [PATCH 41/42] use obsidian.async --- lua/obsidian/cache.lua | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index 021a35ae..a4d783d6 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -1,4 +1,4 @@ -local async = require "plenary.async" +local async = require "obsidian.async" local Note = require "obsidian.note" local log = require "obsidian.log" local EventTypes = require("obsidian.filewatch").EventTypes @@ -72,17 +72,21 @@ local create_on_file_change_callback = function() local relative_path = file.absolute_path:gsub(workspace_path .. "/", "") ---@param note obsidian.Note|? - local update_cache_dictionary = function(note) - if note then - ---@type obsidian.cache.CacheNote - local founded_cache = { - absolute_path = file.absolute_path, - aliases = note.aliases, - last_updated = file.stat.mtime.sec, - tags = note.tags, - } - - cache_notes[relative_path] = founded_cache + local update_cache_dictionary = function(err, note) + if err then + log.err("an error occured when reading from a note. " .. err) + else + if note then -- check, because a note can be deleted. If deleted, the note variable is empty. + ---@type obsidian.cache.CacheNote + local founded_cache = { + absolute_path = file.absolute_path, + aliases = note.aliases, + last_updated = file.stat.mtime.sec, + tags = note.tags, + } + + cache_notes[relative_path] = founded_cache + end end left = left - 1 @@ -274,7 +278,12 @@ local get_cache_notes_from_vault = function(callback) callback(created_note_caches) end - local on_note_parsed = function(note) + local on_note_parsed = function(err, note) + if err then + log.err("an error occured when reading from a note. " .. err) + return + end + local absolute_path = note.path.filename local relative_path = absolute_path:gsub(workspace_path .. "/", "") From c5e8ba8671f923f2d0e682e803c84abcef34ec21 Mon Sep 17 00:00:00 2001 From: kostabekre Date: Mon, 1 Sep 2025 15:23:48 +0300 Subject: [PATCH 42/42] disable warning that a string should be passed instead of a number when converting decimal to octal representation --- lua/obsidian/cache.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/obsidian/cache.lua b/lua/obsidian/cache.lua index a4d783d6..a5c2ed24 100644 --- a/lua/obsidian/cache.lua +++ b/lua/obsidian/cache.lua @@ -209,7 +209,9 @@ local enable_filewatch = function() local stat_of_state_folder = uv.fs_stat(obsidian_state_path) if not stat_of_state_folder then - local ok = uv.fs_mkdir(obsidian_state_path, assert(tonumber(755, 8))) + ---@diagnostic disable-next-line: param-type-mismatch + local mode = assert(tonumber(755, 8)) + local ok = uv.fs_mkdir(obsidian_state_path, mode) if not ok then log.err("Couldn't create folder at " .. obsidian_state_path)