├── .gitignore ├── .stylua.toml ├── Makefile ├── .github ├── workflows │ ├── release.yml │ ├── lint-test.yml │ └── docs.yml └── ISSUE_TEMPLATE │ ├── feature_request.yml │ └── bug_report.yml ├── tests ├── github-browse │ └── github_browse_spec.lua └── minimal_init.lua ├── LICENSE ├── plugin └── github-browse.lua ├── doc └── my-template-docs.txt ├── README.md └── lua └── github-browse └── browse.lua /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/plenary.nvim 2 | .luarc.json 3 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 120 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 2 5 | quote_style = "AutoPreferDouble" 6 | no_call_parentheses = false 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS_INIT=tests/minimal_init.lua 2 | TESTS_DIR=tests/ 3 | 4 | .PHONY: test 5 | 6 | test: 7 | @nvim \ 8 | --headless \ 9 | --noplugin \ 10 | -u ${TESTS_INIT} \ 11 | -c "PlenaryBustedDirectory ${TESTS_DIR} { minimal_init = '${TESTS_INIT}' }" 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "release" 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | luarocks-upload: 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: LuaRocks Upload 12 | uses: nvim-neorocks/luarocks-tag-release@v4 13 | env: 14 | LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }} 15 | -------------------------------------------------------------------------------- /tests/github-browse/github_browse_spec.lua: -------------------------------------------------------------------------------- 1 | local gh = require("github-browse.browse") 2 | 3 | describe("setup", function() 4 | it("should work with default values", function() 5 | assert(gh.browse(), "browse successfully") 6 | end) 7 | 8 | it("should work with custom values", function() 9 | gh.setup({ opt = "custom" }) 10 | assert(gh.browse() == "custom", "does not provide custom value") 11 | end) 12 | end) 13 | -------------------------------------------------------------------------------- /tests/minimal_init.lua: -------------------------------------------------------------------------------- 1 | local plenary_dir = os.getenv("PLENARY_DIR") or "/tmp/plenary.nvim" 2 | local is_not_a_directory = vim.fn.isdirectory(plenary_dir) == 0 3 | if is_not_a_directory then 4 | vim.fn.system({ "git", "clone", "https://github.com/nvim-lua/plenary.nvim", plenary_dir }) 5 | end 6 | 7 | vim.opt.rtp:append(".") 8 | vim.opt.rtp:append(plenary_dir) 9 | 10 | vim.cmd("runtime plugin/plenary.vim") 11 | require("plenary.busted") 12 | -------------------------------------------------------------------------------- /.github/workflows/lint-test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | on: [push, pull_request] 3 | name: lint-test 4 | 5 | jobs: 6 | stylua: 7 | name: stylua 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: JohnnyMorganz/stylua-action@v3 12 | with: 13 | version: latest 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | args: --color always --check lua 16 | 17 | test: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | nvim-versions: ['stable', 'nightly'] 22 | name: test 23 | steps: 24 | - name: checkout 25 | uses: actions/checkout@v3 26 | 27 | - uses: rhysd/action-setup-vim@v1 28 | with: 29 | neovim: true 30 | version: ${{ matrix.nvim-versions }} 31 | 32 | - name: run tests 33 | run: make test 34 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # on: 2 | # push: 3 | # branches: 4 | # - main 5 | # name: docs 6 | 7 | # jobs: 8 | # docs: 9 | # runs-on: ubuntu-latest 10 | # steps: 11 | # - uses: actions/checkout@v3 12 | # - name: panvimdoc 13 | # uses: kdheepak/panvimdoc@main 14 | # with: 15 | # vimdoc: my-template-docs 16 | # version: "Neovim >= 0.8.0" 17 | # demojify: true 18 | # treesitter: true 19 | # - name: Push changes 20 | # uses: stefanzweifel/git-auto-commit-action@v4 21 | # with: 22 | # commit_message: "auto-generate vimdoc" 23 | # commit_user_name: "github-actions[bot]" 24 | # commit_user_email: "github-actions[bot]@users.noreply.github.com" 25 | # commit_author: "github-actions[bot] " 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ellison 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature 3 | title: "feature: " 4 | labels: [enhancement] 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Did you check the docs? 9 | description: Make sure you read all the docs before submitting a feature request 10 | options: 11 | - label: I have read all the docs 12 | required: true 13 | - type: textarea 14 | validations: 15 | required: true 16 | attributes: 17 | label: Is your feature request related to a problem? Please describe. 18 | description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 19 | - type: textarea 20 | validations: 21 | required: true 22 | attributes: 23 | label: Describe the solution you'd like 24 | description: A clear and concise description of what you want to happen. 25 | - type: textarea 26 | validations: 27 | required: true 28 | attributes: 29 | label: Describe alternatives you've considered 30 | description: A clear and concise description of any alternative solutions or features you've considered. 31 | - type: textarea 32 | validations: 33 | required: false 34 | attributes: 35 | label: Additional context 36 | description: Add any other context or screenshots about the feature request here. 37 | -------------------------------------------------------------------------------- /plugin/github-browse.lua: -------------------------------------------------------------------------------- 1 | if vim.fn.has("nvim-0.7.0") == 0 then 2 | vim.api.nvim_err_writeln("github-browse requires at least nvim-0.7.0.1") 3 | return 4 | end 5 | 6 | -- make sure this file is loaded only once 7 | if vim.g.loaded_github_browse == 1 then 8 | return 9 | end 10 | vim.g.loaded_github_browse = 1 11 | 12 | -- create any global command that does not depend on user setup 13 | -- usually it is better to define most commands/mappings in the setup function 14 | -- Be careful to not overuse this file! 15 | local commands = { 16 | repo = function(_) 17 | require("github-browse.browse").browse_repo() 18 | end, 19 | -- TODO: enable ability to copy link to clipboard 20 | line = function(_) 21 | require("github-browse.browse").browse_line() 22 | end, 23 | commit = function(opts) 24 | require("github-browse.browse").browse_commit(opts) 25 | end, 26 | -- TODO: Implement ability to list PRs 27 | pr = function() 28 | require("github-browse.browse").browse_prs() 29 | end, 30 | } 31 | 32 | vim.api.nvim_create_user_command("GithubBrowse", function(opts) 33 | local f = commands[opts.fargs[1]] 34 | if f ~= nil then 35 | f(opts.args) 36 | return 37 | end 38 | 39 | vim.api.nvim_err_writeln(string.format("[github-browse] unknown command: %s", opts.fargs[1])) 40 | end, { 41 | nargs = 1, 42 | complete = function(ArgLead, CmdLine, CursorPos) 43 | return { "repo", "line", "commit", "pr" } 44 | end, 45 | }) 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug/issue 3 | title: "bug: " 4 | labels: [bug] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Before** reporting an issue, make sure to read the documentation and search existing issues. Usage questions such as ***"How do I...?"*** belong in Discussions and will be closed. 10 | - type: checkboxes 11 | attributes: 12 | label: Did you check docs and existing issues? 13 | description: Make sure you checked all of the below before submitting an issue 14 | options: 15 | - label: I have read all the plugin docs 16 | required: true 17 | - label: I have searched the existing issues 18 | required: true 19 | - label: I have searched the existing issues of plugins related to this issue 20 | required: true 21 | - type: input 22 | attributes: 23 | label: "Neovim version (nvim -v)" 24 | placeholder: "0.8.0 commit db1b0ee3b30f" 25 | validations: 26 | required: true 27 | - type: input 28 | attributes: 29 | label: "Operating system/version" 30 | placeholder: "MacOS 11.5" 31 | validations: 32 | required: true 33 | - type: textarea 34 | attributes: 35 | label: Describe the bug 36 | description: A clear and concise description of what the bug is. Please include any related errors you see in Neovim. 37 | validations: 38 | required: true 39 | - type: textarea 40 | attributes: 41 | label: Steps To Reproduce 42 | description: Steps to reproduce the behavior. 43 | placeholder: | 44 | 1. 45 | 2. 46 | 3. 47 | validations: 48 | required: true 49 | - type: textarea 50 | attributes: 51 | label: Expected Behavior 52 | description: A concise description of what you expected to happen. 53 | validations: 54 | required: true 55 | -------------------------------------------------------------------------------- /doc/my-template-docs.txt: -------------------------------------------------------------------------------- 1 | *my-template-docs.txt* For Neovim >= 0.8.0 Last change: 2023 August 17 2 | 3 | ============================================================================== 4 | Table of Contents *my-template-docs-table-of-contents* 5 | 6 | 1. A Neovim Plugin Template |my-template-docs-a-neovim-plugin-template| 7 | - Using it |my-template-docs-a-neovim-plugin-template-using-it| 8 | - Features and structure|my-template-docs-a-neovim-plugin-template-features-and-structure| 9 | 10 | ============================================================================== 11 | 1. A Neovim Plugin Template *my-template-docs-a-neovim-plugin-template* 12 | 13 | 14 | 15 | A template repository for Neovim plugins. 16 | 17 | 18 | USING IT *my-template-docs-a-neovim-plugin-template-using-it* 19 | 20 | Via `gh` 21 | 22 | > 23 | $ gh repo create my-plugin -p ellisonleao/nvim-plugin-template 24 | < 25 | 26 | Viagithub web page: 27 | 28 | Click on `Use this template` 29 | 30 | 31 | 32 | 33 | FEATURES AND STRUCTURE*my-template-docs-a-neovim-plugin-template-features-and-structure* 34 | 35 | - 100% Lua 36 | - Github actions for: 37 | - running tests using plenary.nvim and busted 38 | - check for formatting errors (Stylua) 39 | - vimdocs autogeneration from README.md file 40 | - luarocks release (LUAROCKS_API_KEY secret configuration required) 41 | 42 | 43 | PLUGIN STRUCTURE ~ 44 | 45 | > 46 | . 47 | lua 48 |    plugin_name 49 |       module.lua 50 |    plugin_name.lua 51 | Makefile 52 | plugin 53 |    plugin_name.lua 54 | README.md 55 | tests 56 |    minimal_init.lua 57 |    plugin_name 58 |    plugin_name_spec.lua 59 | < 60 | 61 | ============================================================================== 62 | 2. Links *my-template-docs-links* 63 | 64 | 1. *GitHub Workflow Status*: https://img.shields.io/github/actions/workflow/status/ellisonleao/nvim-plugin-template/lint-test.yml?branch=main&style=for-the-badge 65 | 2. *Lua*: https://img.shields.io/badge/Made%20with%20Lua-blueviolet.svg?style=for-the-badge&logo=lua 66 | 3. **: https://docs.github.com/assets/cb-36544/images/help/repository/use-this-template-button.png 67 | 68 | Generated by panvimdoc 69 | 70 | vim:tw=78:ts=8:noet:ft=help:norl: 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Browse Neovim Extension 2 | 3 | A small Neovim wrapper around the GitHub CLI `gh browse` tool improve workflows when switching between Neovim and GitHub to enable faster sharing links to lines, repos or commits. 4 | 5 | ## Requirements 6 | 7 | - A recent version of Vim or Neovim 8 | - The [Github CLI](https://cli.github.com/) 9 | 10 | ## Installation 11 | 12 | ``` 13 | -- Lazy 14 | { 'josephwoodward/github-browse.nvim' } 15 | ``` 16 | 17 | ## Usage 18 | 19 | The plugin defines commands that wrap the functionality of zoxide: 20 | 21 | - `:GithubBrowse repo`: Opens the current GitHub repository in your default web browser. 22 | - `:GithubBrowse line`: Opens the current line your cursor is Github using your default web browser. 23 | - `:GithubBrowse commit`: Opens given commit in GitHub in your default web browser. 24 | - `:GithubBrowse pr`: Opens selected pull request in GitHub in your default web browser. 25 | 26 | ## Demos 27 | 28 | ### Browse to Line (`:GithubBrowse line`) 29 | 30 | Go to the current line in GitHub: 31 | 32 | ![browse-line](https://github.com/josephwoodward/github-browse.nvim/assets/1237341/8cfffe4d-775e-4efa-ab1b-f8aaa3db0bef) 33 | 34 | You can also use the `copy_line()` function to create a keymap that copies the line number URL directly to your clipboard. 35 | 36 | ```lua 37 | keymap.set('n', 'gbl', function() 38 | require('github-browse.browse').copy_line() 39 | end) 40 | ``` 41 | 42 | --- 43 | 44 | ### Browse to Repository (`:GithubBrowse repo`) 45 | 46 | Load the current repository in GitHub: 47 | 48 | ![browse-repo](https://github.com/josephwoodward/github-browse.nvim/assets/1237341/aac84232-79ab-49dc-9434-c64405695c8c) 49 | 50 | --- 51 | 52 | ### (`:GithubBrowse commit`) 53 | 54 | Go to the current commit in GitHub: 55 | 56 | ![browse-commit](https://github.com/josephwoodward/github-browse.nvim/assets/1237341/1e455938-9a21-492e-abbe-58720cb9ee0c) 57 | 58 | Integration with Telescope's git picker can be achived using the following snippet: 59 | 60 | ```lua 61 | pickers = { 62 | git_commits = { 63 | attach_mappings = function(_, map) 64 | map({ 'i', 'n' }, '', function(_, _) 65 | local entry = require('telescope.actions.state').get_selected_entry() 66 | require('github-browse.browse').browse_commit { args = entry.value } 67 | end) 68 | 69 | -- needs to return true if you want to map default_mappings and 70 | -- false if not 71 | return true 72 | end, 73 | }, 74 | ... 75 | } 76 | ``` 77 | -------------------------------------------------------------------------------- /lua/github-browse/browse.lua: -------------------------------------------------------------------------------- 1 | ---@class Config 2 | ---@field opt string Your config option 3 | local config = {} 4 | 5 | ---@class BrowseModule 6 | local M = {} 7 | 8 | local function execute_command(cmd) 9 | vim.fn.jobstart({ "gh", cmd }) 10 | end 11 | 12 | ---@type Config 13 | M.config = config 14 | 15 | ---@param args Config? 16 | M.setup = function(args) 17 | M.config = vim.tbl_deep_extend("force", M.config, args or {}) 18 | end 19 | 20 | --- Open the current GitHub repository in your default browser. 21 | M.browse_repo = function() 22 | execute_command("browse") 23 | end 24 | 25 | local function open_in_browser(path) 26 | local cmd 27 | if vim.fn.has("mac") == 1 then 28 | cmd = { "open", path } 29 | elseif vim.fn.has("unix") == 1 then 30 | cmd = { "xdg-open", path } 31 | elseif vim.fn.has("win32") == 1 then 32 | cmd = { "cmd.exe", "/c", "start", path } 33 | else 34 | vim.notify("GithubBrowse: Unsupported system for opening browser.", vim.log.levels.ERROR) 35 | return 36 | end 37 | 38 | vim.fn.jobstart(cmd, { detach = true }) 39 | end 40 | 41 | M.browse_prs = function() 42 | local pickers = require("telescope.pickers") 43 | local finders = require("telescope.finders") 44 | local conf = require("telescope.config").values 45 | local actions = require("telescope.actions") 46 | local action_state = require("telescope.actions.state") 47 | 48 | local list_prs = function(opts, entries) 49 | opts = opts or {} 50 | 51 | local finder = finders.new_table({ 52 | results = entries, 53 | entry_maker = function(entry) 54 | return { 55 | value = entry.value, 56 | display = entry.branch .. " (#" .. entry.number .. ")" .. " - " .. entry.display, 57 | ordinal = entry.number, 58 | } 59 | end, 60 | }) 61 | 62 | pickers 63 | .new(opts, { 64 | prompt_title = "View Pull Requests", 65 | finder = finder, 66 | attach_mappings = function(prompt_bufnr, _) 67 | actions.select_default:replace(function() 68 | actions.close(prompt_bufnr) 69 | open_in_browser(action_state.get_selected_entry().value .. "/files") 70 | end) 71 | return true 72 | end, 73 | sorter = conf.generic_sorter(opts), 74 | }) 75 | :find() 76 | end 77 | 78 | local cb = function(_, data, event) 79 | if event == "exit" then 80 | -- TODO: handle this gracefully 81 | elseif event == "stdout" or event == "stderr" then 82 | local entries = {} 83 | local items = vim.fn.json_decode(data) 84 | for _, item in ipairs(items) do 85 | -- print(vim.inspect(item)) 86 | 87 | -- local name = "" 88 | -- if isempty(foo) then 89 | -- foo = "default value" 90 | -- end 91 | 92 | table.insert(entries, { 93 | value = item.url, 94 | display = item.title, 95 | branch = item.headRefName, 96 | number = item.number, 97 | -- author = item.author.name 98 | }) 99 | end 100 | 101 | list_prs({}, entries) 102 | end 103 | end 104 | 105 | vim.fn.jobwait({ 106 | vim.fn.jobstart("gh pr list --json number,title,url,author,headRefName --limit 20", { 107 | on_stdout = cb, 108 | stdout_buffered = true, 109 | stderr_buffered = true, 110 | }), 111 | }) 112 | end 113 | 114 | ---@param opts object 115 | M.browse_commit = function(opts) 116 | local commit = opts.args or "" 117 | if commit == "" then 118 | -- return "Please specify commit" //TODO: Write test case for this 119 | vim.notify("GithubBrowse: please specify a commit", vim.log.levels.ERROR) 120 | return 121 | end 122 | 123 | vim.fn.jobstart({ "gh", "browse", commit }) 124 | end 125 | 126 | --- Open file and line number under the course in your browser. 127 | ---@param opts object 128 | M.browse_line = function(opts) 129 | local cursor_pos, _ = unpack(vim.api.nvim_win_get_cursor(0)) 130 | local file = vim.fn.expand("%:.") 131 | vim.fn.jobstart({ "gh", "browse", string.format("%s:%s", file, cursor_pos) }) 132 | end 133 | 134 | M.copy_line = function(opts) 135 | local cursor_pos, _ = unpack(vim.api.nvim_win_get_cursor(0)) 136 | local file = vim.fn.expand("%:.") 137 | 138 | -- vim.fn.jobstart({ "gh", "browse", string.format("gh browse %s:%s --no-brower", file, cursor_pos)) 139 | local cb = function(_, data, event) 140 | if event == "exit" then 141 | -- TODO: handle this gracefully 142 | elseif event == "stdout" or event == "stderr" then 143 | vim.fn.setreg('"', data) 144 | vim.fn.setreg("+", data) 145 | vim.notify("-> line copied to clipboard") 146 | end 147 | end 148 | 149 | vim.fn.jobwait({ 150 | vim.fn.jobstart(string.format("gh browse %s:%s --no-browser", file, cursor_pos), { 151 | on_stdout = cb, 152 | stdout_buffered = true, 153 | stderr_buffered = true, 154 | }), 155 | }) 156 | end 157 | 158 | return M 159 | --------------------------------------------------------------------------------