Zk grep 2 (inheriting #171)
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
rgcommand, instead ofzk list --format=oneline -mcommand. (to getlnumandcol)
TODO:
- [ ] Sorting order is incorrect in snacks_picker
Now
score -> idx. Sorting byscore -> title(filename) -> lnum -> color 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.
ChangeLog:
- Use built-in hl groups 'TelescopeResults*' instead of custom 'ZkGrep*'
- Updated README.md to reflect these changes
Now using:
TelescopeResultsIdentifierfor titleTelescopeResultsLineNrfor lnum:colTelescopeResultsNormalfor 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,
})
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.
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.
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.
It works!
- Add
greptosnacks_pickernot onlytelescope - Merged
bufname_from_yamlbranch
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.
- doc/zk.txt should be modified (Adding grep feature)
I don't really understand how to handle vimdoc...
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.
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.
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
ZkNotesuseszk listcommand. It accepts--notebook-dirarg. (See:$ zk list --help)- The
zk.listAPI passesnotebook_pathto--notebook-dir. - The
zk listAPI sets the zk root aspath or util.resolve_notebook_path(bufnr) - But, my
ZkGrepusesripgrepin telescope or snacks_picker, and bypass the API commandzk list --interactive.
Call order (Just a note for me)
- zk.pick_notes()
- api.list()
- 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:
greprequiresnotebook_pathpickrequiresnoteslist.- An unnecessary
zk listcommand is excuted before everyshow_grep_pickeroperation.
It will take a little longer.
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.
Finished zk.txt and README.md.
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.
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 revealworks 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.)
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?
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
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.
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?)
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😂)
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
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.
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
[!NOTE] I later decided to use
zk.apiinstead oflyaml, 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.listapi (No need to implementlyaml) - vim.b.zk_title keeps the title
- vim.o.zk_list table keeps the
zk listoutput as JSON (zk.listapi 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-nvimlike below?
bufferline = {
enabled = true,
select = { 'id', 'absPath', 'title', 'filenameStem' },
custom_title = function(note)
return note.title or note.filenameStem or note.id
end,
}
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.
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.
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)
I've merged README.md updates from upstream.
No conflict, I guess.
(Finally this PR include purely grep feature only.)
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.
The count is because
lessis being used as a pager to display the results. You need to pass the--quietoption.
Got it -- there's an option to hide the count!
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 ! ☺️
@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.
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.