From e45cdbc7c4f8618a92ef6d81a409669dbbdf5f6b Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Fri, 24 Apr 2026 12:02:46 +0300 Subject: [PATCH] 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. --- runtime/doc/pack.txt | 4 +-- runtime/lua/vim/_core/util.lua | 37 ++++++++++++++++++++++++++++ runtime/lua/vim/pack.lua | 4 +-- runtime/lua/vim/pack/_lsp.lua | 32 +++++++++--------------- test/functional/plugin/pack_spec.lua | 14 +++++------ 5 files changed, 59 insertions(+), 32 deletions(-) diff --git a/runtime/doc/pack.txt b/runtime/doc/pack.txt index f01da75f7f..9ec47b9fb4 100644 --- a/runtime/doc/pack.txt +++ b/runtime/doc/pack.txt @@ -511,8 +511,8 @@ update({names}, {opts}) *vim.pack.update()* 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()|) - diff --git a/runtime/lua/vim/_core/util.lua b/runtime/lua/vim/_core/util.lua index 29ba8c5720..e5dd5c2068 100644 --- a/runtime/lua/vim/_core/util.lua +++ b/runtime/lua/vim/_core/util.lua @@ -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:////" +--- @param target string Identifier of a target, like commit hash or tag name +--- @param target_type "commit"|"tag" +--- @return string? # Example: /releases/tag/ +function M.get_forge_url(repo, target, target_type) + -- The structure // 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 diff --git a/runtime/lua/vim/pack.lua b/runtime/lua/vim/pack.lua index c6dec53d32..932e6ca070 100644 --- a/runtime/lua/vim/pack.lua +++ b/runtime/lua/vim/pack.lua @@ -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 diff --git a/runtime/lua/vim/pack/_lsp.lua b/runtime/lua/vim/pack/_lsp.lua index 869b0cf73f..b560183492 100644 --- a/runtime/lua/vim/pack/_lsp.lua +++ b/runtime/lua/vim/pack/_lsp.lua @@ -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 } } diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua index 755b3039a6..05e784f43c 100644 --- a/test/functional/plugin/pack_spec.lua +++ b/test/functional/plugin/pack_spec.lua @@ -1473,12 +1473,12 @@ describe('vim.pack', function() } assert_links(ref_file_links) - -- Mock using GitHub sources which should provide all links + -- Mock using common sources which should provide all links local fetch_github = 'https://github.com/user/fetch' - local semver_github = 'https://github.com/user/semver' + local semver_codeberg = 'https://codeberg.org/user/semver' local lines = api.nvim_buf_get_lines(0, 0, -1, false) lines[13] = lines[13]:gsub('^(Source: +)(.+)$', '%1' .. fetch_github) - lines[26] = lines[26]:gsub('^(Source: +)(.+)$', '%1' .. semver_github) + lines[26] = lines[26]:gsub('^(Source: +)(.+)$', '%1' .. semver_codeberg) api.nvim_set_option_value('modifiable', true, { buf = 0 }) api.nvim_buf_set_lines(0, 0, -1, false, lines) api.nvim_set_option_value('modifiable', false, { buf = 0 }) @@ -1492,11 +1492,11 @@ describe('vim.pack', function() { 18, 2, 18, 8, fetch_github .. '/commit/' .. lines[19]:match('^> (%S+)') }, { 19, 2, 19, 8, fetch_github .. '/commit/' .. lines[20]:match('^> (%S+)') }, { 24, 10, 24, 10 + semver_path:len() - 1, semver_path_uri }, -- Path: ... - { 25, 10, 25, 39, semver_github }, -- Source: ... - { 26, 10, 26, 49, semver_github .. '/commit/' .. lines[27]:match('(%S+) %b()$') }, -- Revision: ... + { 25, 10, 25, 41, semver_codeberg }, -- Source: ... + { 26, 10, 26, 49, semver_codeberg .. '/commit/' .. lines[27]:match('(%S+) %b()$') }, -- Revision: ... -- Should use UTF index - { 29, 2, 29, 7, semver_github .. '/releases/tag/v1.0.0' }, - { 30, 2, 30, 6, semver_github .. '/releases/tag/0.3.1' }, + { 29, 2, 29, 7, semver_codeberg .. '/src/tag/v1.0.0' }, + { 30, 2, 30, 6, semver_codeberg .. '/src/tag/0.3.1' }, } assert_links(ref_github_links)