Custom search for v2 using git grep

Hi all, just wanted to share my custom search I’ve come up with since downloading SilverBullet last night. I’m finding this really helpful and I’ve added this to a Library/Custom/Search page.

command.define {
  name = "Search: Custom Search",
  key = "Ctrl-s",
  run = function()
    local search_term = editor.prompt("Search:", "")
    local search_results = search(search_term, false)
    local filter_options = {}
    for _, item in ipairs(search_results) do
      table.insert(filter_options, { name = "[" .. item.file .. "] " .. item.search_result, value = item })
    end
    local result = editor.filterBox("Select:", filter_options)
    if result and result.value then
      local page, count = string.gsub(result.value.file, ".md", "")
      editor.navigate({ kind = "page", page = page })
      editor.moveCursorToLine(result.value.line, result.value.col, true)
    end
  end
}

function search(search_term, use_template)
  use_template = (use_template ~= false)
  local search_results = shell.run("git", {"grep", "--line-number", "--column", search_term})
  local parts = string.split(search_results.stdout, "\n")
  local output = {}

  for _, item in ipairs(parts) do
    local fullpath, line, col, result = string.match(item, "^(.-):(.-):(.-):(.+)$")
    if fullpath then
      local search_result = string.trim(result)
      if not string.startsWith(search_result, "${search(") then
        table.insert(output, { file = fullpath, search_result = search_result, line = line, col = col })
      end
    end
  end

  if not use_template then
    return output
  else
    local template_output = {}

    for _, item in ipairs(output) do
      local page, count = string.gsub(item.file, ".md", "")
      local t = { file = page, search_result = item.search_result, line = item.line, col = item.col }
      table.insert(template_output, dom.li({
        onclick = function()
          editor.navigate({ kind = "page", page = t.file })
          editor.moveCursorToLine(t.line, t.col, true)
        end,
        dom.a({
          href = "javascript:void(0);",
          "[" .. t.file .. ":" .. t.line .. ":" .. t.col .. "] - " .. t.search_result
        })
      }))
    end

    return widget.new({
      cssClasses = {"template-basic"},
      html = dom.div({
        class = "search-list",
        dom.ul(template_output)
      }),
    })
  end
end
.template-basic .wrapper,
.template-basic.sb-lua-directive-inline {
  border: 0 !important;
  margin: 0 !important;
  padding: 0 !important;
}

.search-list {
  border: 0;
  padding: 0;
}

.search-list li {
  margin-bottom: 8px;
}

You can then use this in a few ways:

${search("some search term")} on any page which will render an inline clickable list that brings you directly to the line and column of the search term:

Or via the custom Search: Custom Search command that I’ve bound to ctrl-s. This is nice as it asks you for a search term and then shows all results in a picker window:

This also brings you directly to the search result and has the nice added bonus of being filterable.

3 Likes

It should also be noted that this will only work if you have your space folder in git as it utilizes the git binary in the docker image.

This is easily updatable if you run this out of the docker image to use something like ripgrep.

This is amazing!

I adapted it to use ripgrep (rg). Since I run SilverBullet in a container, I do need to make the executable available first. I do this by adding the volume mapping /usr/bin/rg:/usr/bin/rg (since I have rg installed on the host anyway).

Here’s my adaptation for ripgrep:

command.define {
  name = "Search: Custom Search",
  key = "Ctrl-s",
  run = function()
    local term = editor.prompt()
    local results = rg(term)
    local options = {}
    for result in results do
      table.insert(options, { 
        name = result.file,
        description = result.match,
        value = result
      })
    end
    local result = editor.filterBox("Select:", options)
    if result and result.value then
      local page, count = string.gsub(result.value.file, ".md", "")
      editor.navigate({ kind = "page", page = page })
      editor.moveCursorToLine(result.value.line, result.value.col, true)
    end
  end
}

function rg(term)
  local search_results = shell.run("rg", {"-nb", "--type", "markdown", term})
  local matches = string.split(search_results.stdout, "\n")
  local output = {}
  for item in matches do
    local fullpath, line, col, result = string.match(item, "^(.-):(.-):(.-):(.+)$")
    if (fullpath == nil) then
      do break end
    end
    table.insert(output, {file = fullpath, line = line, col = col, match = result })
  end

  return output
end

Which in my current theme, looks like this:

(Yes, I need to tweak my highlight colour, it’s not making the text more readable… :D)

Edit: swapping the name and description properties is actually nicer, because most of the time the matching line is more important than the page it’s on.

4 Likes

Thanks for these tweaks. I’ll definitely be pulling a few of these back in.

This is super nice.

Protip: In v2 I implemented Gist exports: Library/Std/Gist (and imports) using this, you can export a library like this to a Github Gist, which others can then easily import based on their URL using Import: URL. Example, here’s my Git library as a Gist: git.md · GitHub

You might want to do some error handling, because this will quietly fail if the function does not return something expected, or when you are offline. Not ideal for people who want to customize it even more.

Thanks for the implementation and remix!

My spin:

command.define {
  name = "Search Recursively",
  key = "Ctrl-s",
  run = function()
    local term = editor.prompt()
    local results = rg(term)
    local selection = {}

    if results == 0 then
      return editor.flashNotification("Nothing found", "warning")
    end
    
    for result in results do
      table.insert(selection, { 
        name = result.text,
        description = result.path,
        value = result
      })
    end
    
    local result = editor.filterBox("Select:", selection, "Found: " .. #results .. " entries")
    
    if result and result.value then
      local page, count = string.gsub(result.value.path, ".md", "")
      editor.navigate({ kind = "page", page = page })
      editor.moveCursorToLine(result.value.row, result.value.column, true)
    end
  end
}

function rg(term)
  local args = {"-nb", "--type", "markdown", term}
  local found, results = pcall(shell.run, "rg", args)
  
  if found then
    local matches = string.split(results.stdout, "\n")
    local results = {}
    
    for match in matches do
      local path, row, column, text = string.match(match, "^(.-):(.-):(.-):(.+)$")
      if (path ~= nil and row ~= nil and column ~= nil and text ~= nil) then
        table.insert(results, { path = path, row = row, column = column, text = text })
      end
    end
    
    return results
  else
    return 0 
  end
end

This is amazing, thank you all. As a new user I was disappointed by the search capabilities – all you get is a title without context, in a space with thousands of pages this is of no help – and am relieved to see this working well.

Slight bug: your string.gsub() should operate on result.value.path instead of result.value.file - took me a while to spot when pressing enter on a search result was throwing an error!

woops, fixed.
That’s what I get from mindlessly copy-pasting from the original.
The one is use in my instance is a bit different but had to remove some stuff to keep same functionality and remove some comments :slight_smile:

1 Like