ABC - Accurate 🖇️ Bi-directional 🖱️ Cursor-level linking system

A discovery while experimenting with AnyType
revealed a conceptual equivalence to SilverBullet:

promoting a plain paragraph in AnyType to a named object mirrors
converting an ordinary text in SilverBullet’s editor into an [[aspiring-page]].

Opening that created object in AT (or [[aspiring-page]] in SB) shows backlinks
one of which (back)links to the originating article at line level (at least),
while others point to other pages at line level (as well).

From a higher-level perspective,
this pattern constitutes a cursor–page–cursor bidirectional link:
the page functions as an intermediary that permits cursor-to-cursor connections.

Conclusion: a single [[aspiring-page]] suffices to establish cursor–cursor links;

nevertheless, the earlier code pattern required n+1 distinct [[aspiring-pages]] for 1 forward + n inverse labels.

Inspired by this, I refactored the final code snippet above to minimize anchors/labels, thereby reducing SB’s indexing overhead and better reflecting some essence of graph theory.

function getSelectedText()
  local sel = editor.getSelection()
  if not sel or sel.from == sel.to then return nil end
  local text = editor.getText()
  return text:sub(sel.from + 1, sel.to)
end

function setSelectedText(newText)
  local sel = editor.getSelection()
  if not sel or sel.from == sel.to then return nil end
  editor.replaceRange(sel.from, sel.to, newText)
end

function usrPrompt(hinText, iniText)
  local iniText = iniText or ""
  local input = editor.prompt(hinText, iniText)
  if not input then
    editor.flashNotification("Cancelled", "warn")
  end
  return input
end

local anchorSymbol = "⚓"
local suffixFlabel = "➡️"
local suffixBlabel = "🔙"
local siblings = "🧑‍🤝‍🧑"

-- =========== Forth Anchor + Back Refs ==================

local function tableBack(Flabel)
  local aspiringPage = Flabel .. anchorSymbol
  return query[[
    from index.tag "link"
    where toPage == aspiringPage and alias:find(suffixFlabel, 1, true)
    order by _.thBlabel
    select {ref=_.ref, thBlabel=_.thBlabel}
  ]]
end

function backRefs(Flabel)
  local str = template.each(tableBack(Flabel), template.new[==[​[[${_.ref}|${_.thBlabel}]]​]==])
  if #str == 0 then return "No BackRef" end
  return str
end

command.define {
  name = "Insert: ForthAnchor + BackRefs",
  key = "Ctrl-,",
  run = function()
    local iniText = getSelectedText()
    -- local Flabel = usrPrompt('Enter: label (to be Referred)', iniText)
    local Flabel
    if iniText and iniText ~= "" then
      Flabel = iniText
    else
      Flabel = usrPrompt('Enter: label (to be Referred)', '')
    end
    if not Flabel then return end
    local aspiringPage = Flabel .. anchorSymbol
    local forthAnchor = "[[" .. aspiringPage .. "||^|" .. suffixBlabel .. "]]"
    local backRefs = '${backRefs("' .. Flabel .. '")}'
    local fullText = forthAnchor .. backRefs
    if iniText and iniText ~= "" then
      setSelectedText("") -- Delete selected iniText
    end
    editor.insertAtPos(fullText, editor.getCursor(), true)
    editor.copyToClipboard(Flabel)
  end
}

-- =========== Back Anchor + Forth Ref ==================

local function tableBack_noSelf(Flabel, thBlabelNum)
  local aspiringPage = Flabel .. anchorSymbol
  return query[[
    from index.tag "link"
    where toPage == aspiringPage and alias:find(suffixFlabel, 1, true) and thBlabelNum ~= _.thBlabel
    order by _.thBlabel
    select {ref=_.ref, thBlabel=_.thBlabel}
  ]]
end

function backRefs_noSelf(Flabel, thBlabelNum)
  local str = template.each(tableBack_noSelf(Flabel, thBlabelNum), template.new[==[​[[${_.ref}|${_.thBlabel}]]​]==])
  if #str == 0 then return "No Sibling" end
  return str
end

local function tableForth(Flabel)
  local aspiringPage = Flabel .. anchorSymbol
  return query[[
    from index.tag "link"
    where toPage == aspiringPage and alias:find(suffixBlabel, 1, true)
    select {ref=_.ref}
  ]]
end

function forthRef(Flabel)
  local str = template.each(tableForth(Flabel), template.new("[[${_.ref}|​" .. siblings .. "​]]"))
  if #str == 0 then return "No such Anchor" end
  return str
end

command.define {
  name = "Insert: BackAnchor + ForthRef",
  key = "Ctrl-.",
  run = function()
    local alias = getSelectedText()
    local iniText = js.window.navigator.clipboard.readText()
    -- local Flabel = usrPrompt('Jump to: label', iniText)
    local Flabel
    if iniText and iniText ~= "" then
      Flabel = iniText
    else
      Flabel = usrPrompt('Jump to: label', '')
    end
    if not Flabel then return end
    local thBlabelNum = #tableBack(Flabel) + 1
    local aspiringPage = Flabel .. anchorSymbol
    local backAnchor = "[[" .. aspiringPage .. "||^|" .. suffixFlabel .. thBlabelNum .. "]]"
    local forthRef = '${forthRef("' .. Flabel .. '")}'
    local backRefs_noSelf = '${backRefs_noSelf("' .. Flabel .. '",' .. thBlabelNum .. ')}'
    local fullText = backAnchor .. forthRef .. backRefs_noSelf
    if alias and alias ~= "" then
      setSelectedText("") -- Delete selected alias
    else
      alias = ''
    end
    editor.insertAtPos(fullText, editor.getCursor(), true)
    editor.insertAtCursor(alias, false) -- scrollIntoView?
  end
}

index.defineTag {
  name = "link",
  metatable = {
    __index = function(self, attr)
      if attr == "thBlabel" then
        return tonumber(string.match(self.alias, suffixFlabel .. "([0-9]+)"))
      end
    end
  }
}

Now the forward jump is be triggered through the siblings emoji :people_holding_hands: (previously numbers ahead of :people_holding_hands:) — an undeniably odd interaction — but the trade-off is a likely improvement in indexing performance.


While experimenting with these apps, I drafted a comparison table that is neither rigorous[1] nor entirely fair[2]:

App Core RAM Tech
SilverBullet ~ 24 MB Go + TS + Lua
RoamEdit ~ 44 MB Custom Web Stack ?
Workflowy ~ 73 MB ? Custom Web Stack ?
RoamResearch -[3] Electron + …
RemNote -[4] Electron + …
Tana ~ 233 MB Electron ? + …
SiYuan ~ 231 MB Electron + Go + TS
AnyType 170 ~ 330 MB Electron + TS
LogSeq ~ 230 MB Electron + Clojure + TS
Orca ~ 200 MB Electron + …
Obsidian ~ 160 MB Electron + TS ?

My fondness for Sublime and Zed over VS Code has left me slightly biased against Electron, which influenced the table above; my apologies if it seems offensive. Note that the above is not a ranking (i.e. an ordered list) but an unordered list.


  1. since I am not a computer specialist ↩︎

  2. it does not account for the RAM inside browser tabs consumed by SB and RE ↩︎

  3. not tested ↩︎

  4. not tested ↩︎

1 Like