feat(forge): support custom shorthand prefixes

Problem: forge shorthand parsing hardcoded `%l%l` (exactly 2 lowercase
letters), preventing custom prefixes like `github:`. Completions also
hardcoded `gh:`, `gl:`, `cb:` patterns.

Solution: iterate `_by_shorthand` keys dynamically in `_parse_shorthand`
instead of matching a fixed pattern. Build completion patterns from
`forge.backends()`. Add `shorthand` field to `ForgeInstanceConfig` so
users can override prefixes via config, applied in `_ensure_instances()`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Barrett Ruth 2026-03-10 22:20:27 -04:00
parent e62f2f818c
commit 61eadec87e
4 changed files with 106 additions and 8 deletions

View file

@ -1,4 +1,5 @@
local config = require('pending.config')
local forge = require('pending.forge')
---@class pending.CompletionItem
---@field word string
@ -109,6 +110,17 @@ local function recur_completions()
return result
end
---@param source string
---@return boolean
function M._is_forge_source(source)
for _, b in ipairs(forge.backends()) do
if b.shorthand == source then
return true
end
end
return false
end
---@type string?
local _complete_source = nil
@ -128,10 +140,10 @@ function M.omnifunc(findstart, base)
{ vim.pesc(dk) .. ':([%S]*)$', dk },
{ 'cat:([%S]*)$', 'cat' },
{ vim.pesc(rk) .. ':([%S]*)$', rk },
{ 'gh:([%S]*)$', 'gh' },
{ 'gl:([%S]*)$', 'gl' },
{ 'cb:([%S]*)$', 'cb' },
}
for _, b in ipairs(forge.backends()) do
table.insert(checks, { vim.pesc(b.shorthand) .. ':([%S]*)$', b.shorthand })
end
for _, check in ipairs(checks) do
local start = before:find(check[1])
@ -172,7 +184,7 @@ function M.omnifunc(findstart, base)
table.insert(matches, { word = c.word, menu = '[' .. source .. ']', info = c.info })
end
end
elseif source == 'gh' or source == 'gl' or source == 'cb' then
elseif M._is_forge_source(source) then
local s = require('pending.buffer').store()
if s then
local seen = {}

View file

@ -37,6 +37,7 @@
---@field icon? string
---@field issue_format? string
---@field instances? string[]
---@field shorthand? string
---@class pending.ForgeConfig
---@field auto_close? boolean

View file

@ -62,6 +62,14 @@ function M.backends()
return _backends
end
function M._reset_instances()
_instances_resolved = false
_by_shorthand = {}
for _, b in ipairs(_backends) do
_by_shorthand[b.shorthand] = b
end
end
local function _ensure_instances()
if _instances_resolved then
return
@ -73,17 +81,27 @@ local function _ensure_instances()
for _, inst in ipairs(forge_cfg.instances or {}) do
_by_host[inst] = backend
end
if forge_cfg.shorthand and forge_cfg.shorthand ~= backend.shorthand then
_by_shorthand[backend.shorthand] = nil
backend.shorthand = forge_cfg.shorthand
_by_shorthand[backend.shorthand] = backend
end
end
end
---@param token string
---@return pending.ForgeRef?
function M._parse_shorthand(token)
local prefix, rest = token:match('^(%l%l):(.+)$')
if not prefix then
return nil
_ensure_instances()
local backend, rest
for prefix, b in pairs(_by_shorthand) do
local candidate = token:match('^' .. vim.pesc(prefix) .. ':(.+)$')
if candidate then
backend = b
rest = candidate
break
end
end
local backend = _by_shorthand[prefix]
if not backend then
return nil
end

View file

@ -391,6 +391,73 @@ describe('forge registry', function()
end)
end)
describe('custom forge prefixes', function()
local config = require('pending.config')
local complete = require('pending.complete')
it('parses custom-length shorthand (3+ chars)', function()
local custom = forge.gitea_backend({
name = 'customforge',
shorthand = 'cgf',
default_host = 'custom.example.com',
})
forge.register(custom)
local ref = forge._parse_shorthand('cgf:alice/proj#99')
assert.is_not_nil(ref)
assert.equals('customforge', ref.forge)
assert.equals('alice', ref.owner)
assert.equals('proj', ref.repo)
assert.equals(99, ref.number)
end)
it('parse_ref dispatches custom-length shorthand', function()
local ref = forge.parse_ref('cgf:alice/proj#5')
assert.is_not_nil(ref)
assert.equals('customforge', ref.forge)
assert.equals(5, ref.number)
end)
it('find_refs finds custom-length shorthand', function()
local refs = forge.find_refs('Fix cgf:alice/proj#12')
assert.equals(1, #refs)
assert.equals('customforge', refs[1].ref.forge)
assert.equals(12, refs[1].ref.number)
end)
it('completion returns entries for custom backends', function()
assert.is_true(complete._is_forge_source('cgf'))
end)
it('config shorthand override re-registers backend', function()
vim.g.pending = {
forge = {
github = { shorthand = 'github' },
},
}
config.reset()
forge._reset_instances()
local ref = forge._parse_shorthand('github:user/repo#1')
assert.is_not_nil(ref)
assert.equals('github', ref.forge)
assert.equals('user', ref.owner)
assert.equals('repo', ref.repo)
assert.equals(1, ref.number)
assert.is_nil(forge._parse_shorthand('gh:user/repo#1'))
vim.g.pending = nil
config.reset()
for _, b in ipairs(forge.backends()) do
if b.name == 'github' then
b.shorthand = 'gh'
end
end
forge._reset_instances()
end)
end)
describe('forge diff integration', function()
local store = require('pending.store')
local diff = require('pending.diff')