zk-nvim icon indicating copy to clipboard operation
zk-nvim copied to clipboard

Zk grep 2 (inheriting #171)

Open riodelphino opened this issue 7 months ago • 33 comments

Initially, I added this sample code to implement the :ZkGrep command.

Then I merged the changes from #171 into the latest main branch, and extended it based on the above example.

Features:

  • Grep with telescope and snacks.picker
  • Highlighted entries for better readability
  • Jumps directly to the matched line
  • Above features are solved by using rg command, instead of zk list --format=oneline -m command. (to get lnum and col)

TODO:

  • [ ] Sorting order is incorrect in snacks_picker Now score -> idx. Sorting by score -> title(filename) -> lnum -> col or modifying scoring directly is ideal. snacks_picker's sorting is difficult to understand, so I give up for now. This may helps (Japanese)

Thanks to @shfattig for providing a great foundation and roadmap for integrating grep into zk-nvim pickers.

riodelphino avatar May 15 '25 18:05 riodelphino

ChangeLog:

  • Use built-in hl groups 'TelescopeResults*' instead of custom 'ZkGrep*'
  • Updated README.md to reflect these changes

Now using:

  • TelescopeResultsIdentifier for title
  • TelescopeResultsLineNr for lnum:col
  • TelescopeResultsNormal for body text

To customize these highlights, you can add something like this to your config:

vim.api.nvim_create_autocmd('ColorScheme', {
  callback = function()
    vim.api.nvim_set_hl(0, 'TelescopeResultsIdentifier', { fg = '#E0B449', bold = true })
    vim.api.nvim_set_hl(0, 'TelescopeResultsLineNr', { fg = '#999999' })
    vim.api.nvim_set_hl(0, 'TelescopeResultsNormal', { fg = '#CCCCCC' })
  end,
})

riodelphino avatar May 16 '25 04:05 riodelphino

This is awesome, thanks for the work. I really want to get grep functionality in here. I'm sad that #171 stalled out. Using zk's match was great because it's insanely fast (faster than rg), but without jumping to or highlighting the results, it came at a price.

All that being said, I have to prioritise bug fixes much more highly at the moment as until the start of July I need to finish my masters thesis, while working part time on the side. So just focusing on bug fixes of existing functionality keeps my other commitments manageable, while keeping the ship afloat here.

But I am totally in to get this merged, and will look at it when I can, but latest in July. Unless one of the other maintainers pokes their head back in. But it's getting quieter and quieter.

tjex avatar May 16 '25 07:05 tjex

Thank you for replying even though you're so busy. And I'm glad that you see value in this.

Yes, zk list is probably quite fast. If only that command could return lnum and col, it would be ideal.

Of course, please prioritize bug fixes — I’d be happy if you could take a look when you have some spare time.

In the meantime, I might continue adding commits here with further improvements. See you in July or later!

Concerns (for future reference):

Currently, ZkGrep is integrated into ui.pick_notes(). However, this makes ui.pick_notes() more complex due to the added if-then-else logic. I'm considering whether it would be better to create a separate function, such as ui.pick_grep_notes(), instead.

riodelphino avatar May 16 '25 11:05 riodelphino

And now I'm looking into integrating grep with snacks.picker. I had a hard time understanding snacks.nvim's code. I'll probably add a commit soon.

riodelphino avatar Jun 25 '25 19:06 riodelphino

It works!

  • Add grep to snacks_picker not only telescope
  • Merged bufname_from_yaml branch

An Issue:

In snacks_picker, lines containing : — such as this is the error line 12:30:15 — cause a parsing error in the rg command.

The line is incorrectly parsed as path = path/to/file.md:100:1:this is the error line 12 lnum = 30 col = 15

telescope's grep does not have same issue. Because the parsing regex was modifiable from outside.

snacks_picker need to be forked to recieve --json option to rg command, or parse_regex option. I think these solve this issue.

riodelphino avatar Jul 19 '25 16:07 riodelphino

  • doc/zk.txt should be modified (Adding grep feature)

I don't really understand how to handle vimdoc...

riodelphino avatar Jul 30 '25 04:07 riodelphino

Great, thanks for all of this. I'm still around, just a lot on so will still need some time to get to this. Hopefully in the coming weeks I can start to get my head back in here. In the middle of moving apartments and getting to grips with the new full time job.

I would say for the vimdoc, that you can just add it as a built in command to section 1.5 Built-in Commands. Just follow the same pattern there that the other commands are using.

Maybe it doesn't make sense to point to the passable API arguments, unless zk grep is implementing the zk.list api for example.

tjex avatar Jul 30 '25 09:07 tjex

Thanks a lot for your guidance !!

My TODO:

  • Add to zk/doc.txt.
  • ~~Fix the slow display on neo-tree with too many files. (Maybe caused by checking zk dir or not, for every file)~~ (Already solved in the next comming commit.)
  • Add grep feature with fzf's live_grep ? (Just a plan)

It will take some time.

riodelphino avatar Aug 06 '25 17:08 riodelphino

Issue

~Cannot change the notebook root when using :ZkGrep.~ SOLVED!

Description

I've found a way to change the zk notebook dir with :ZkNotes or :ZkGreps.

For example, from the ~/Documents/zk/default directory:

🔵 This works: :ZkNotes { notebook_path = '~/Documents/zk/todo' }

We can change the notebook dir. However:

❌️ This does not work: :ZkGrep { notebook_path = '~/Documents/zk/todo' }

Oops... Let me organize it.

Reason

  • ZkNotes uses zk list command. It accepts --notebook-dir arg. (See: $ zk list --help)
  • The zk.list API passes notebook_path to --notebook-dir.
  • The zk list API sets the zk root as path or util.resolve_notebook_path(bufnr)
  • But, my ZkGrep uses ripgrep in telescope or snacks_picker, and bypass the API command zk list --interactive.

Call order (Just a note for me)

  1. zk.pick_notes()
  2. api.list()
  3. ui.pick_notes()

What to do next ?

This is quite complex! But I'm trying to fix this, to enable :ZkGrep even in such rare cases.

I think we need to completely separate the pick note and grep note functions, since their structure and requirements are fundamentally different.

Specifically:

  • grep requires notebook_path
  • pick requires notes list.
  • An unnecessary zk list command is excuted before every show_grep_picker operation.

It will take a little longer.

riodelphino avatar Aug 08 '25 15:08 riodelphino

Enabled grepping another notebook

I think above issue was solved. Now :ZkGrep and zk.grep_notes() also can show another zk notebook.

:ZkGrep { notebook_path = 'path/to/another_zk_notebook'}
require('zk').grep_notes( { notebook_path = 'path/to/another_zk_notebook'} )

Splitted grep and pick

To avoid complexity, grep and pick are separated. zk.edit also checks if the options include { grep = true } or not, to branch into grep or pick.

Unacceptable changes?

I made many modifying in this commit with trial and error. Please let me know if there are unacceptable changes.

riodelphino avatar Sep 01 '25 10:09 riodelphino

Finished zk.txt and README.md.

riodelphino avatar Sep 01 '25 13:09 riodelphino

I also modified the explanations for sample codes and documents for bufferline and neo-tree (from #237 PR, in README.md and zk.txt). To make it easier to read.

riodelphino avatar Sep 01 '25 14:09 riodelphino

Issue

:Neotree reveal not works with the sample code... It does not select current buffer file automatically.

But now I found neo-tree-zk.nvim.

  • :Neotree reveal works fine
  • Faster (using zk.api)
  • Supports all queries
  • It doesn’t cause any slowdown in typical projects.

x Git highlighting is not supported now.

I think better to use it and remove all neo-tree sample code from this PR.

I may fork & extend it someday if I feel like it, to show specific file title and support git highlights. (e.g. John Davis / This is my book (2025) from YAML's title, author and published.)

riodelphino avatar Sep 02 '25 15:09 riodelphino

But neotree-zk hasn't been active for a long time. Probably better to support the upstream? Makes it complicated for users to have to install a special flavour of neotree to use zk?

tjex avatar Sep 05 '25 09:09 tjex

But neotree-zk hasn't been active for a long time. Probably better to support the upstream?

Yes, neo-tree-zk should support upstream neo-tree, because it currently throws some errors. (But basically it works.)

Makes it complicated for users to have to install a special flavour of neotree to use zk?

No, I don't think so.

It’s easier for users (and for me) to install a dedicated flavor like neo-tree-zk and just add some keymaps. However, modifying neo-tree config a lot is kind of annoying.

And I found neo-tree-zk has a lot of features. You can see them with n key in :Neotree zk

Screen shot 2025-09-10 2 22 59

The issue is the coding effort required to update neo-tree-zk and understand neo-tree 😂

I'm currently looking at the neo-tree filesystem source and neo-tree-zk zk source. It seems like a lot has changed in the last 3 years. It will take a lot of work.

riodelphino avatar Sep 05 '25 15:09 riodelphino

Anyway, now I think my sample codes for neo-tree and bufferline are outside of the scope of this PR. To avoid delaying the merge, I'm going to remove it for now. Adding them to another PR is better, right? (or modifying neo-tree-zk)

(I merged the upstream changes to this branch, to solve the conflicts. Is it a correct behavior?)

riodelphino avatar Sep 08 '25 04:09 riodelphino

Removed the sample codes. (They were kept in my local note.) So now it's clean and ready for merge!

(Oh sorry. Originaly, removed sample codes belong to my #237 PR. So this removing needs to be reverted here, I think... I'm confused with my PRs😂)

riodelphino avatar Sep 14 '25 07:09 riodelphino

Hey. Thanks for all the work here. I'm seeing it all, just in the last week of madness. Have my thesis presentation next monday, and then on holiday for a few weeks. So will be looking at this and the custom buffer titles PR from next week on :D

tjex avatar Sep 15 '25 15:09 tjex

Oh, no. Now I found zk command has a feature that parses YAML frontmatter. 💧 And probably it's faster than lyaml.

I'm checking the codes and considering whether to remove lyaml or not. 😂

I had to check the zk cli documentation...

Sample zk commands to get YAML

All the zk file:

zk list --format json | jq -r '.[] | del(.body, .rawContent)'

Specific zk file:

# Without .body and .rawContent
zk list --format json notes/i7bfl1.md | jq -r '.[] | del(.body, .rawContent)'
{
  "filename": "i7bfl1.md",
  "filenameStem": "i7bfl1",
  "path": "notes/i7bfl1.md",
  "absPath": "/Users/username/Documents/zk/notes/i7bfl1.md",
  "title": "This is the title",
  "link": "[This is the title](notes/i7bfl1)",
  "lead": "# This is the first line in the file",
  "snippets": [
    "# This is the first line in the file"
  ],
  "wordCount": 127,
  "tags": [
    "my_tag1",
    "my_tag2"
  ],
  "metadata": {
    "categories": [],
    "created": "2025-08-30 18:50:47",
    "id": "i7bfl1",
    "modified": "2025-08-30 19:07:19",
    "slug": null,
    "status": "draft",
    "tags": [
      "my_tag1",
      "my_tag2"
    ],
    "title": "This is the title"
  },

Found 1 note
  "created": "2025-08-30T09:50:47.592119375Z",
  "modified": "2025-08-30T10:07:19.158909276Z",
  "checksum": "cf0f27a3a8f18a5b3ae6da8a3d4b6c9b6f7dee8181e330d815c62e4b4f5d215d"
}

The root section keeps title, tags and path. The metadata section keeps all fields fetched from YAML frontmatter.

riodelphino avatar Sep 17 '25 07:09 riodelphino

Humm... sooooo dificult... Unfortunately, using lyaml is so much easier.

Async or Sync?

zk.api.list is async, but bufferline.nvim does not accept async. It costs too many codings to bridge between them for me. Other users would be annoyed for adding such huge code in bufferline's config.

--> Solved by this comment

Not accepts single note path?

zk list cli command accepts notebook dir and also single file path (This is OK) But zk.api.list or zk lsp accept only notebook dir and returns all file's infomation. So need to search all files every time. ~~(probably it's an issue in zk-nvim or zk cli.)~~

--> Solved by this comment

Returns broken JSON?

The bash command like zk list --format json {file_path} | jq ... always returns JSON with Found {n} note text in the top or the bottom. It breaks JSON format. The zk document does not seem to discribe how to disable the unnecessary text message. ~~(Should create an issue in zk cli?)~~

--> Solved by this comment

riodelphino avatar Sep 17 '25 14:09 riodelphino

[!NOTE] I later decided to use zk.api instead of lyaml, and split the bufferline part into separate PR. See next comment and the next one for details.


I think this API version is better than lyaml version (#237)

The speed seems to be almost same, or API is faster.

Compared Points lyaml API
Speed o o
Return JSON as lua table o o
Fetch YAML frontmatter o o
Fetch user defined extra YAML field o o
Catch tags: in YAML o o
Catch #tags in body o
Treat all zk note information o
Accepts single note path *1 o o
Can fetch single note *1 o o
Compatibillity for bufferline o o
Effortless config for bufferline *2 o o
Async o
Sync o

*1 API implevement is needed. However, hrefs option can solve it. *2 I've found the configuration is easy with api too.

This is the code for it.

Concepts

  • Using builtin zk.list api (No need to implement lyaml)
  • vim.b.zk_title keeps the title
  • vim.o.zk_list table keeps the zk list output as JSON (zk.list api can't load a single note. So have to keep the whole list at once.)
  • Update zk_list and zk_title in BufWritePost, then refresh bufferline

Code

bufferline config with lazy.nvim:

config = function()
   require('bufferline').setup({
      -- ...
      -- other configs
      -- ...
      name_formatter = function(buf)
         local ok, zk_title = pcall(vim.api.nvim_buf_get_var, buf.bufnr, 'zk_title')
         if ok and zk_title then
            return zk_title
         else
            vim.schedule(function()
               update_zk_list(function()
                  if refresh_title(buf) then require('bufferline.ui').refresh() end
               end)
            end)
         end
         return vim.fn.fnamemodify(buf.name, ':t:r')
      end,
   })

   -- Refresh Buffer Title
   function refresh_title(buf, callback)
      -- Parse title
      if vim.g.zk_list then
         for _, note in ipairs(vim.g.zk_list) do
            if note.absPath == buf.path then
               local title = note.title or note.filenameStem or note.id
               vim.api.nvim_buf_set_var(buf.bufnr, 'zk_title', title)
               if callback then vim.schedule(callback) end
               return true
            end
         end
      end
      return false
   end
   
   -- Async update zk_list
   function update_zk_list(callback)
      require('zk.api').list(nil, {
         select = { 'id', 'absPath', 'title', 'filenameStem' },
      }, function(err, notes)
         if not err and notes then
            vim.g.zk_list = notes
         else
            print('zk_list update error:', err)
         end
            if callback then vim.schedule(callback) end
      end)
   end
   
   -- Add BufWritePost autocmd
   vim.api.nvim_create_autocmd('BufWritePost', {
      pattern = '*.md',
      callback = function()
         local bufnr = vim.api.nvim_get_current_buf()
         local path = vim.api.nvim_buf_get_name(bufnr)
         local buf = { bufnr = bufnr, path = path }
   
         -- Clear existing zk_title
         vim.b[bufnr].zk_title = nil
   
         -- Update zk_list -> update zk_title -> refresh
         update_zk_list(function()
            refresh_title(buf, function()
               require('bufferline.ui').refresh()
            end)
         end)
      end,
   })
end,

Options

  • Should these code be Included in zk-nvim ?
  • If yes, should I extend configs in zk-nvim like below?
  bufferline = {
     enabled = true,
     select = { 'id', 'absPath', 'title', 'filenameStem' },
     custom_title = function(note)
          return note.title or note.filenameStem or note.id
     end,
  }

riodelphino avatar Sep 17 '25 14:09 riodelphino

After some investigations, now I understand that we don't need lyaml any more. zk.api.list with hrefs option is enough for that purpose.

e.g.

local notebook_root = "/path/to/notebook"
local opts = {
   hrefs = { "/path/to/single/note.md" }, -- Get zk info from the specific file(s)
   select = {  "title", "path", "absPath"  }, -- Fields to get
}
require('zk.api').list(
   notebook_root, -- Even if a zk file path is set, zk.api converts it into notebook root dir.
   opts,
   function(err, notes)
      -- callback function
   end
)

So I will replace lyaml with zk.list in this PR.

riodelphino avatar Sep 18 '25 15:09 riodelphino

Anyway, the discussion about YAML and Bufferline is a bit off-topic here.

So I created another PR for them. I recommend to merge the new PR #260, and please close #237 lyaml PR.

riodelphino avatar Sep 18 '25 15:09 riodelphino

Now I replaced lyaml totally with zk.api.list in Telescope and snacks_picker ! (Store notes from zk.api.list as M.notes_cache table.)

It's quite faster than lyaml version ^^

(Ready to be merged)

riodelphino avatar Sep 21 '25 10:09 riodelphino

I've merged README.md updates from upstream. No conflict, I guess. (Finally this PR include purely grep feature only.)

riodelphino avatar Sep 26 '25 17:09 riodelphino

The bash command like zk list --format json {file_path} | jq ... always returns JSON with Found {n} note text in the top or the bottom. It breaks JSON format.

The count is because less is being used as a pager to display the results. You need to pass the --quiet option.

tjex avatar Oct 01 '25 22:10 tjex

The count is because less is being used as a pager to display the results. You need to pass the --quiet option.

Got it -- there's an option to hide the count!

riodelphino avatar Oct 02 '25 00:10 riodelphino

Thanks for reviewing !

Additionally, I created PR in neo-tree-zk, for syncing upstream. https://github.com/zk-org/neo-tree-zk.nvim/pull/2

Though it took quite a while, it's pretty close to completion, too.

Anyway, please get some rest first @tjex ! ☺️

riodelphino avatar Oct 04 '25 04:10 riodelphino

@WhyNotHugo Would you be able to give the code of this pr a look? Need your lua / nvim plugin competencies on this, it's a pretty big addition and would be great to get it right. Also as it's a long standing requested feature.

tjex avatar Oct 12 '25 22:10 tjex

I've only had a glance at a high level for now.

Do we need to use ripgrep for the search? zk itself seems to be capable of handling searches via zk list --match.


I'm not very familiar with snacks. Do we really need to introduce dependency on a specific picker implementation? Typically plugins use vim.ui.select, and let users register their own preferred picker (or use the default).

Folks who prefer snacks.picker can use:

require("snacks").setup({
  picker = {
    ui_select = true,
  },
})

Personally, I use fzf_lua:

require("fzf-lua").register_ui_select()

I'm sure there are other implementations out there too.

WhyNotHugo avatar Oct 13 '25 11:10 WhyNotHugo