From 61eadec87e5da2fff6a9a63b16859b3f13af7b03 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 10 Mar 2026 22:20:27 -0400 Subject: [PATCH] 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 --- lua/pending/complete.lua | 20 +++++++++--- lua/pending/config.lua | 1 + lua/pending/forge.lua | 26 +++++++++++++--- spec/forge_spec.lua | 67 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 8 deletions(-) diff --git a/lua/pending/complete.lua b/lua/pending/complete.lua index 135d1a4..98291ce 100644 --- a/lua/pending/complete.lua +++ b/lua/pending/complete.lua @@ -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 = {} diff --git a/lua/pending/config.lua b/lua/pending/config.lua index b1ab639..5a139cb 100644 --- a/lua/pending/config.lua +++ b/lua/pending/config.lua @@ -37,6 +37,7 @@ ---@field icon? string ---@field issue_format? string ---@field instances? string[] +---@field shorthand? string ---@class pending.ForgeConfig ---@field auto_close? boolean diff --git a/lua/pending/forge.lua b/lua/pending/forge.lua index 9b32655..baebc0a 100644 --- a/lua/pending/forge.lua +++ b/lua/pending/forge.lua @@ -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 diff --git a/spec/forge_spec.lua b/spec/forge_spec.lua index fac8021..8bd4162 100644 --- a/spec/forge_spec.lua +++ b/spec/forge_spec.lua @@ -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')