fix(util): add and use forge link computation

Problem: There are many Git forges each with a different way of
  constructing permanent links to like commits and tags.

Solution: Add a private utility function that computes these special
  links on the best effort basis.
This commit is contained in:
Evgeni Chasnovski
2026-04-24 12:02:46 +03:00
parent d01dc690e1
commit e45cdbc7c4
5 changed files with 59 additions and 32 deletions

View File

@@ -129,4 +129,41 @@ function M.term_exitcode()
return ''
end
--- Compute a link to a target on a forge host
--- @param repo string URL of repo, usually "https://<domain>/<user>/<name>"
--- @param target string Identifier of a target, like commit hash or tag name
--- @param target_type "commit"|"tag"
--- @return string? # Example: <repo>/releases/tag/<target>
function M.get_forge_url(repo, target, target_type)
-- The structure <host>/<middle>/<target> works for most forges. Like:
-- - https://github.com/neovim/nvim-lspconfig/commit/e146efa
-- - https://github.com/neovim/nvim-lspconfig/releases/tag/v2.8.0
local ref_middles = {
{ pattern = '^https://github%.com/', commit = 'commit', tag = 'releases/tag' },
{ pattern = '^https://gitlab%.com/', commit = '-/commit', tag = '-/tags' },
{ pattern = '^https://git%.sr%.ht/', commit = 'commit', tag = 'refs' },
{ pattern = '^https://tangled%.org/', commit = 'commit', tag = 'tags' },
{ pattern = '^https://bitbucket%.org/', commit = 'commits', tag = 'src' },
-- Fall back to Forgejo style since there is no fixed host
{ pattern = '^https://', commit = 'commit', tag = 'src/tag' },
}
local middle = ''
for _, mid in ipairs(ref_middles) do
if repo:match(mid.pattern) then
middle = mid[target_type]
if middle ~= '' then
break
end
end
end
if middle == '' then
return nil
end
repo = repo:gsub('/+$', '')
return ('%s/%s/%s'):format(repo, middle, target)
end
return M

View File

@@ -1248,8 +1248,8 @@ end
---
--- Some features are provided via LSP:
--- - 'textDocument/documentLink' - compute links for plugin paths, sources,
--- commits, and tags. Use |gx| to open a link to an object at cursor.
--- Only supports GitHub hosted repositories for commit and tag links.
--- commits, and tags. Makes a best effort educated guess about a link structure.
--- Use |gx| to open a link to an object at cursor.
--- - 'textDocument/documentSymbol' (`gO` via |lsp-defaults| or |vim.lsp.buf.document_symbol()|) -
--- show structure of the buffer.
--- - 'textDocument/hover' (`K` via |lsp-defaults| or |vim.lsp.buf.hover()|) - show more

View File

@@ -83,42 +83,32 @@ end
--- @param src string Plugin source
--- @return vim.pack.lsp.DocumentLink? # A link structure according to the LSP specification
local function match_link(line, pattern, link_type, lnum, src)
-- Only support GitHub for now. Maybe other forges in the future.
local is_github = vim.startswith(src, 'https://github.com/')
if (link_type == 'commit' or link_type == 'tag') and not is_github then
return nil
end
--- @type number?, string?, number?
local from, match, to = line:match(pattern)
if not (from and match and to) then
return
return nil
end
-- Convert to UTF index used in LSP positions
from = vim.str_utfindex(line, 'utf-16', from - 1, false)
to = vim.str_utfindex(line, 'utf-16', to - 2, false)
-- Reference:
-- - https://github.com/neovim/nvim-lspconfig/commit/e146efa
-- - https://github.com/neovim/nvim-lspconfig/commit/e146efacbafed3789ac568abcc5a981c5decaa58
-- - https://github.com/neovim/nvim-lspconfig/releases/tag/v2.8.0
--- @type string?
local target = match
if link_type == 'commit' or link_type == 'tag' then
local src_noslash = src:gsub('/+$', '')
local middle = link_type == 'commit' and 'commit' or 'releases/tag'
target = ('%s/%s/%s'):format(src_noslash, middle, match)
---@diagnostic disable-next-line: param-type-mismatch
target = require('vim._core.util').get_forge_url(src, match, link_type)
elseif link_type == 'path' then
target = vim.uri_from_fname(match)
end
return {
range = {
start = { line = lnum - 1, character = from },
['end'] = { line = lnum - 1, character = to },
},
target = target,
}
if target == nil then
return nil
end
local start = { line = lnum - 1, character = from }
local end_ = { line = lnum - 1, character = to }
return { range = { start = start, ['end'] = end_ }, target = target }
end
--- @param params { textDocument: { uri: string } }