🔖 Tagss Picker

Practical tag search handles multi-tag intersections, e.g. Marijn Haverbeke's blog

Command

-- priority: 11
command.define {
  name = "Navigate: Tags Picker",
  key = "Ctrl-Alt-T",
  run = function()
    local allTags = query[[from index.tag "tag" select {name = _.name}]]
    local selectedNames = {}
    while true do
      local availableOptions = {}
      for _, tagObj in ipairs(allTags) do
        if not table.includes(selectedNames, tagObj.name) then
          table.insert(availableOptions, tagObj)
        end
      end
      if #availableOptions == 0 then
        break
      end
      local description = "Select a Tag"
      local placeholder = "🔖 a Tag"
      if #selectedNames > 0 then
        description = "Selected Tags ⏺️:" .. table.concat(selectedNames, ", ") .. " ➕ (ESC to Go)"
        placeholder = string.rep("🔖", #selectedNames) .. " a Tag"
      end
      local selection = editor.filterBox("🤏 Pick", availableOptions, description, placeholder)
      if selection then
        table.insert(selectedNames, selection.name)
      else
        if #selectedNames == 0 then
          return
        else
          break
        end
      end
    end
    local targetPage = "tags:" .. table.concat(selectedNames, ",")
    editor.navigate(targetPage)
  end
}

Virtual Page

-- priority: 11
virtualPage.define {
  pattern = "tags:(.+)",
  run = function(inputString)
    local rawTags = inputString:split(",")
    local tags = {}
    for _, t in ipairs(rawTags) do
      local cleanTag = t:trim()
      if cleanTag ~= "" then
        table.insert(tags, cleanTag)
      end
    end

    if #tags == 0 then return "No tags specified." end

    local text = ""
    local tagName = tags[1]
    local allObjects = query[[
      from index.tag(tagName)
      order by ref
    ]]
    
    if #tags == 1 then
      text = "# Objects tagged with: " .. tagName .. "\n"
      local tagParts = tagName:split("/")
      local parentTags = {}
      for i in ipairs(tagParts) do
        local slice = table.pack(table.unpack(tagParts, 1, i))
        if i ~= #tagParts then
          table.insert(parentTags, {name=table.concat(slice, "/")})
        end
      end
      if #parentTags > 0 then
        text = text .. "## Parent tags\n"
          .. template.each(parentTags, templates.tagItem)
      end
      local subTags = query[[
        from index.tag "tag"
        where string.startsWith(_.name, tagName .. "/")
        select {name=_.name}
      ]]
      if #subTags > 0 then
        text = text .. "## Child tags\n"
          .. template.each(subTags, templates.tagItem)
      end
    else
      text = "# Objects tagged with: " .. table.concat(tags, ", ") .. "\n"
      for i = 2, #tags do
        allObjects = query[[
          from allObjects
          where table.includes(_.tags, tags[i])
        ]]
      end
    end
    
    local taggedPages = {}
    local taggedTasks = {}
    local taggedItems = {}
    local taggedData = {}
    local taggedParagraphs = {}

    -- improve performance 
    for _, obj in ipairs(allObjects) do
      if obj.itags and table.includes(obj.itags, "page") then
        table.insert(taggedPages, obj)
      end
      if obj.itags and table.includes(obj.itags, "task") then
        table.insert(taggedTasks, obj)
      end
      if obj.itags and table.includes(obj.itags, "item") then
        table.insert(taggedItems, obj)
      end
      if obj.itags and table.includes(obj.itags, "data") then
        table.insert(taggedData, obj)
      end
      if obj.itags and table.includes(obj.itags, "paragraph") then
        table.insert(taggedParagraphs, obj)
      end
    end

    if #taggedPages > 0 then
      text = text .. "## Pages\n"
        .. template.each(taggedPages, templates.pageItem)
    end
    
    if #taggedTasks > 0 then
      text = text .. "## Tasks\n"
        .. template.each(taggedTasks, templates.taskItem)
    end
    
    if #taggedItems > 0 then
      text = text .. "## Items\n"
        .. template.each(taggedItems, templates.itemItem)
    end
    
    if #taggedData > 0 then
      text = text .. "## Data\n"
        .. markdown.objectsToTable(taggedData) .. "\n"
    end
    
    if #taggedParagraphs > 0 then
      text = text .. "## Paragraphs\n"
        .. template.each(taggedParagraphs, templates.paragraphItem)
    end

    return text
  end
}
2 Likes

Currently, the picker displays all available tags at once. Could the functionality be extended so that the selection list narrows dynamically? Specifically, if a user selects a tag, the list should update to show only the tags that appear on objects already associated with that first tag.

The workflow would look like this:

  • The picker shows all tags initially.
  • The user selects Tag A.
  • The system identifies all objects tagged with Tag A.
  • The picker updates to display only the additional tags present on those specific objects.

This faceted filtering approach would help guide users toward valid data intersections and prevent users from selecting tag combinations that yield zero results.

Nice work, by the way! :clap:

1 Like

oh, logical, and converge quicker.

I just simply excluded those selected tags from all tags… little bit lazy yes…:thought_balloon:

I’ll follow up~


Edit: Picker needs update, Virtual Page seems don’t have to.
and Virtual Page has already functions pretty much like your logic,
thus borrowed some from it

done:

-- priority: 11
command.define {
  name = "Navigate: Tags Picker",
  key = "Ctrl-Alt-T",
  run = function()
    local selectedNames = {}
    
    while true do
      local potentialTags = {}
      
      if #selectedNames == 0 then
        potentialTags = query[[from index.tag "tag" select {name = _.name}]]
      else
        
        local q = query[[from index.tag(selectedNames[1])]]
        
        for i = 2, #selectedNames do
          q = query[[
            from q where table.includes(_.tags, selectedNames[i])
          ]]
        end
        
        local tagSet = {}
        for _, obj in ipairs(q) do
          if obj.tags and type(obj.tags) == "table" then
            for _, t in ipairs(obj.tags) do
              tagSet[t] = true
            end
          end
        end
        
        -- convert Set to list for Picker
        for tagName, _ in pairs(tagSet) do
          table.insert(potentialTags, {name = tagName})
        end
        
        table.sort(potentialTags, function(a, b) return a.name < b.name end)
      end

      local availableOptions = {}
      for _, tagObj in ipairs(potentialTags) do
        if not table.includes(selectedNames, tagObj.name) then
          table.insert(availableOptions, tagObj)
        end
      end

      if #availableOptions == 0 then
        break
      end

      local description = "Select a Tag"
      local placeholder = "🔖 a Tag"
      if #selectedNames > 0 then
        description = "Selected Tags ⏺️:" .. table.concat(selectedNames, ", ") .. " ➕ (ESC to Go)"
        placeholder = string.rep("🔖", #selectedNames) .. " Filter next tag..."
      end

      local selection = editor.filterBox("🤏 Pick", availableOptions, description, placeholder)
      
      if selection then
        table.insert(selectedNames, selection.name)
      else
        if #selectedNames == 0 then
          return
        else
          break
        end
      end
    end

    local targetPage = "tags:" .. table.concat(selectedNames, ",")
    editor.navigate(targetPage)
  end
}

3 Likes

Well done !
Suggestion : use a single picker for both possibilities (no filtering | filtering according to previous choices) by asking the user to enter 1 or 2 before displaying the list of tags.
Easier said than done. It’s just an idea !
Thanks for the development :grinning_face:

Good work! Works as expected. Thank you! :slight_smile:

MJF’s suggestion is in line with the ‘shortest path to the destination’, I think, thus we should adopt it.

ok i understand.
But, i coded a version that does what I wanted : first, choose the search mode for the tag list, then choose the tag(s). Here is a screenshot :



-- priority: 11
command.define {
  name = "Navigate: Tags Picker",
  key = "Ctrl-Alt-a",
  run = function()
    local selectedNames = {}

    local description = "Specify whether the list of tags should be filtered during selection..."
    local placeholder = ""
    local availableOptions = {}
    table.insert(availableOptions, {name="1- No filter. Always, the complete list", value=1 })
    table.insert(availableOptions, {name="2- Yes. Filter by the tags found in objects meeting the criteria", value=2 })
    local mode = editor.filterBox("🔀Search mode", availableOptions, description, placeholder)
    
    if not mode then
      return
    end

    local allTags = query[[from index.tag "tag" select {name = _.name}]]
	
    while true do
	  if mode.value == 2 then
		  local potentialTags = {}
		  
		  if #selectedNames == 0 then
			potentialTags = allTags
		  else
			
			local q = query[[from index.tag(selectedNames[1])]]
			
			for i = 2, #selectedNames do
			  q = query[[
				from q where table.includes(_.tags, selectedNames[i])
			  ]]
			end
			
			local tagSet = {}
			for _, obj in ipairs(q) do
			  if obj.tags and type(obj.tags) == "table" then
				for _, t in ipairs(obj.tags) do
				  tagSet[t] = true
				end
			  end
			end
			
			-- convert Set to list for Picker
			for tagName, _ in pairs(tagSet) do
			  table.insert(potentialTags, {name = tagName})
			end
			
			table.sort(potentialTags, function(a, b) return a.name < b.name end)
		  end
		  allTags = potentialTags
	  end
  
      local availableOptions = {}
      for _, tagObj in ipairs(allTags) do
        if not table.includes(selectedNames, tagObj.name) then
          table.insert(availableOptions, tagObj)
        end
      end
      
      if #availableOptions == 0 then
        break
      end

      local description = ""
      if mode.value == 1 then
        description = "Complete list, always"
        else
        description = "Complete list then filtered list"
      end
      local placeholder = "🔖 a Tag"
      if #selectedNames > 0 then
        description = "Selected Tags ⏺️:" .. table.concat(selectedNames, ", ") .. " ➕ (ESC to Go)"
        placeholder = string.rep("🔖", #selectedNames) .. " a Tag"
      end
      
      local selection = editor.filterBox("🤏 Pick", availableOptions, description, placeholder)
      
      if selection then
        table.insert(selectedNames, selection.name)
      else
        if #selectedNames == 0 then
          return
        else
          break
        end
      end
    end
    
    local targetPage = "tags:" .. table.concat(selectedNames, ",")
    editor.navigate(targetPage)
  end
}


There are now many options for searching for objects (page, tasks, history, etc.). A centralizing interface would be useful, in my opinion. In particular, it would avoid having to remember numerous keyboard shortcuts.

What do you think?

I tried it and it worked fine.
and your idea may have a better application:

Combine tag picker and tags picker into one shortcut key,
and the first picker performs the selection of tag or tags (like yours).

Although the total num of key-press is not reduced, it does save one shortcut key.

1- I don’t understand your proposal : when I assign the same shortcut to both codes, only the second one opens and it does not offer a choice. What’s wrong ?

2- i found a bug in the virtual page : when many tags are selected, only the first is used. The problem is in:

allObjects = query[[
          from allObjects
          where table.includes(_.tags, tags[i])
        ]]

the solution is:

for i = 2, #tags do
      
      local tagValue = tags[i]
        
      allObjects = query[[
          from allObjects
          where table.includes(_.tags, tagValue)
        ]]
end

sorry to reply late.

  1. oh, I meant using your search mode to specify whether using tag picker or tags picker, through your previous invention, rather than just simply applying the same keyboard shortcut to these 2 pickers (otherwise the problem your mentioned will appear).

  2. Yes, you are right, the var name I chose for “tags” overlaps with the attribute name _.tags, which may cause some problems (but I don’t seem to have this problem on version 2.3.0?)

Thanks for the answer !

  1. I don’t understand your explanation. It’s not important …
  2. ok. Nb : i use v.2.3.0.