Created
January 5, 2026 10:34
-
-
Save Zejnilovic/0a4a828bb57febeea37f09b085b17c91 to your computer and use it in GitHub Desktop.
A small Neovim helper that opens GitHub references under the cursor (or visual selection) in a browser. This is editor-native. No plugins, no API calls, no Tree-sitter, no LSP. Just Lua.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --[[ | |
| github_ref.lua | |
| A small, editor-native Neovim helper for opening GitHub references found | |
| under the cursor or in a visual selection. | |
| This file intentionally avoids plugins, GitHub API calls, Tree-sitter, | |
| LSP integration, or background jobs. It is designed to be fast, predictable, | |
| and usable anywhere text appears (code, documentation, comments, or notes). | |
| Usage | |
| ----- | |
| Press <leader>gb in normal or visual mode on any of the following patterns: | |
| `org/repo` - Opens the repository | |
| `org/repo@ref` - Opens a branch, tag, or commit | |
| (shorhand sha and full branch names with slashes are supported) | |
| `org/repo#123` - Opens issue #123 (or PR via GitHub redirect) | |
| `#123` - Resolves the issue via the current git remote and opens it | |
| The mapping works in any buffer and does not depend on filetype. | |
| Visual mode | |
| ----------- | |
| When a visual selection is active, the first valid GitHub reference found | |
| in the selection is used. If no selection is active, the WORD under the | |
| cursor is used instead. | |
| Configuration | |
| ------------- | |
| By default, links are opened against https://github.com. | |
| For GitHub Enterprise or self-hosted GitHub instances: | |
| require("config.github_ref").setup({ | |
| github_base_url = "https://github.mycompany.com", | |
| }) | |
| Git remotes using HTTPS and SSH are supported. | |
| Design notes | |
| ------------ | |
| - No GitHub API usage (issues vs PRs are not distinguished) | |
| - No network requests | |
| - No assumptions about language or filetype | |
| - Deterministic behavior over heuristics | |
| - NB!: Pull requests are opened via /issues/<id> and allowed to redirect naturally | |
| This file is intended to be easy to copy, modify, or later extract into a | |
| proper Neovim plugin if desired. | |
| ]] | |
| local M = {} | |
| M.config = { | |
| github_base_url = "https://github.com", | |
| } | |
| local function open_url(url) | |
| if vim.fn.has("mac") == 1 then | |
| vim.fn.jobstart({ "open", url }, { detach = true }) | |
| elseif vim.fn.has("unix") == 1 then | |
| vim.fn.jobstart({ "xdg-open", url }, { detach = true }) | |
| elseif vim.fn.has("win32") == 1 then | |
| vim.fn.jobstart({ "cmd.exe", "/c", "start", url }, { detach = true }) | |
| end | |
| end | |
| local function get_git_remote() | |
| local handle = io.popen("git config --get remote.origin.url 2>/dev/null") | |
| if not handle then return nil end | |
| local result = handle:read("*a") | |
| handle:close() | |
| return result and result:gsub("%s+", "") or nil | |
| end | |
| local function parse_remote(remote) | |
| if not remote then return nil end | |
| -- git@github.com:org/repo.git | |
| local path = remote:match("^git@[^:]+:(.+)$") | |
| if path then | |
| return path:gsub("%.git$", "") | |
| end | |
| -- ssh://git@github.com/org/repo.git | |
| local path2 = remote:match("^ssh://git@[^/]+/(.+)$") | |
| if path2 then | |
| return path2:gsub("%.git$", "") | |
| end | |
| -- https://github.com/org/repo.git | |
| local path3 = remote:match("^https?://[^/]+/(.+)$") | |
| if path3 then | |
| return path3:gsub("%.git$", "") | |
| end | |
| return nil | |
| end | |
| local function get_visual_selection() | |
| local _, ls, cs = unpack(vim.fn.getpos("'<")) | |
| local _, le, ce = unpack(vim.fn.getpos("'>")) | |
| if ls == 0 then return nil end | |
| local lines = vim.api.nvim_buf_get_lines(0, ls - 1, le, false) | |
| if #lines == 0 then return nil end | |
| lines[#lines] = string.sub(lines[#lines], 1, ce) | |
| lines[1] = string.sub(lines[1], cs) | |
| return table.concat(lines, " ") | |
| end | |
| local function get_token_under_cursor() | |
| local token = vim.fn.expand("<cWORD>") | |
| token = token:gsub("^[%(%[%{\"']+", "") | |
| token = token:gsub("[%]%)}\"',%.]+$", "") | |
| return token | |
| end | |
| local function parse_github(text) | |
| local base = M.config.github_base_url | |
| -- org/repo#123 | |
| local org, repo, issue = text:match("([%w%-_.]+)/([%w%-_.]+)#(%d+)") | |
| if org then | |
| return base .. "/" .. org .. "/" .. repo .. "/issues/" .. issue | |
| end | |
| -- #123 (resolve via git remote) | |
| local issue_only = text:match("^#(%d+)$") | |
| if issue_only then | |
| local remote = get_git_remote() | |
| local repo_path = parse_remote(remote) | |
| if not repo_path then | |
| vim.notify("Cannot resolve #"..issue_only.." (no git remote)", vim.log.levels.ERROR) | |
| return nil | |
| end | |
| return base .. "/" .. repo_path .. "/issues/" .. issue_only | |
| end | |
| -- org/repo@ref (ref may contain slashes) | |
| local org2, repo2, ref = text:match("([%w%-_.]+)/([%w%-_.]+)@(.+)") | |
| if org2 then | |
| return base .. "/" .. org2 .. "/" .. repo2 .. "/tree/" .. ref | |
| end | |
| -- org/repo | |
| local org3, repo3 = text:match("([%w%-_.]+)/([%w%-_.]+)") | |
| if org3 then | |
| return base .. "/" .. org3 .. "/" .. repo3 | |
| end | |
| return nil | |
| end | |
| local function open_github_ref() | |
| local text = nil | |
| if vim.fn.mode():match("[vV]") then | |
| text = get_visual_selection() | |
| end | |
| if not text or text == "" then | |
| text = get_token_under_cursor() | |
| end | |
| local url = parse_github(text) | |
| if not url then | |
| vim.notify("No GitHub reference found", vim.log.levels.WARN) | |
| return | |
| end | |
| open_url(url) | |
| end | |
| -- ========================= | |
| -- Keymaps | |
| -- ========================= | |
| vim.keymap.set({ "n", "v" }, "<leader>gb", open_github_ref, { | |
| desc = "Open GitHub reference", | |
| }) | |
| -- ========================= | |
| -- Optional: public setup | |
| -- ========================= | |
| function M.setup(opts) | |
| M.config = vim.tbl_deep_extend("force", M.config, opts or {}) | |
| end | |
| return M |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment