[
  {
    "path": ".github/workflows/default.yml",
    "content": "name: default\n\non:\n  pull_request:\n  push:\n    branches: [main]\n\njobs:\n  stylua:\n    name: Check code style\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: JohnnyMorganz/stylua-action@v1\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          args: --color always --check .\n\n  selene:\n    name: Lint code\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: NTBBloodbath/selene-action@v1.0.0\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          args: --color always .\n\n  test:\n    name: Neovim test runner\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        neovim-version: [v0.9.0, v0.8.0, v0.7.0, stable, nightly]\n    steps:\n      - uses: actions/checkout@v2\n        with:\n          path: urlview.nvim\n      - uses: actions/checkout@v2\n        with:\n          repository: nvim-lua/plenary.nvim\n          path: plenary.nvim\n      - uses: rhysd/action-setup-vim@v1\n        with:\n          neovim: true\n          version: ${{ matrix.neovim-version }}\n      - run: make test\n        working-directory: urlview.nvim\n        timeout-minutes: 1\n"
  },
  {
    "path": ".gitignore",
    "content": "doc/tags\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Andrew Xie\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "test:\n\tnvim --headless --noplugin -u tests/init.vim -c \"PlenaryBustedDirectory tests/urlview {minimal_init = 'tests/init.vim'}\"\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">🔎 urlview.nvim</h1>\n<p align=\"center\"><i>Find and display URLs from a variety of search contexts</i></p>\n<p align=\"center\">\n  <a href=\"https://github.com/neovim/neovim\">\n    <img alt=\"Neovim Version\" src=\"https://img.shields.io/static/v1?label=&message=%3E%3D0.7&style=for-the-badge&logo=neovim&color=green&labelColor=302D41\"/>\n  </a>\n  <a href=\"https://github.com/axieax/urlview.nvim/stargazers\">\n    <img alt=\"Repo Stars\" src=\"https://img.shields.io/github/stars/axieax/urlview.nvim?style=for-the-badge&color=yellow&label=%E2%AD%90&labelColor=302D41\"/>\n  </a>\n  <a href=\"https://github.com/axieax/urlview.nvim\">\n    <img alt=\"Repo Size\" src=\"https://img.shields.io/github/repo-size/axieax/urlview.nvim?label=&color=orange&logo=hackthebox&style=for-the-badge&logoColor=lightgray&labelColor=302D41\"/>\n  </a>\n</p>\n\n✨ UrlView is an extensible plugin for the [Neovim](https://neovim.io) text editor which essentially:\n\n1. Finds URLs from a variety of **search contexts** (e.g. from a buffer, file, plugin URLs)\n2. Displays these URLs in a **picker**, such as the built-in `vim.ui.select`, [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim), or [fzf-lua](https://github.com/ibhagwan/fzf-lua).\n3. Performs **actions** on selected URLs, such as navigating to the URL in your preferred browser, or copying the link to your clipboard\n\n🎯 Additional features and example use cases include:\n\n- Easily visualise all the URLs in a buffer or file (e.g. links in your Markdown documents)\n- Quickly accessing repo webpages for installed Neovim plugins (life-saver for config updates or browsing plugin documentation)\n- Ability to register custom searchers (e.g. Jira ticket numbers), pickers and actions (please see [docs](doc/urlview.txt) or `:h urlview.search-custom`)\n- Jumping to the previous or next URL in the active buffer (and opening the URL in your browser)\n\n> Please note that currently, this plugin only detects URLs beginning with a `http(s)` or `www` prefix for buffer and file search, but there are plans to support a more general pattern (see [🗺️ Roadmap](https://github.com/axieax/urlview.nvim/issues/3)).\n\n## 📸 Screenshots\n\n### 📋 Buffer Links\n\n`:UrlView` or `:UrlView buffer`\n\n![buffer-demo](https://user-images.githubusercontent.com/62098008/161417569-e8103fc4-a009-4c4f-95a7-ea7e22cbb3df.png)\n\n### 🔌 Plugin Links\n\n`:UrlView lazy`, `:UrlView packer`, or `:UrlView vimplug` depending on your plugin manager of choice\n\n![packer-demo](https://user-images.githubusercontent.com/62098008/161417652-fd514310-a926-4ec7-af28-b2cfa3aa4b19.png)\n\n## ⚡ Requirements\n\n- This plugin supports **Neovim v0.7** or later.\n- Please find the appropriate _\\*-compat_ Git tag if you need legacy support for previous Neovim versions, such as [v0.6-compat](https://github.com/axieax/urlview.nvim/tree/v0.6-compat) for nvim v0.6, although these versions will no longer receive any new updates or features.\n- For Neovim versions prior to v0.6 or Vanilla [Vim](https://www.vim.org/) support, please check out [urlview.vim](https://github.com/strboul/urlview.vim) as an alternative plugin.\n\n## 🚀 Usage\n\n### Searching contexts\n\n1. Use the command `:UrlView` to see all the URLs in the current buffer.\n\n- For your convenience, feel free to setup a keybind for this using `vim.keymap.set`:\n\n  ```lua\n  vim.keymap.set(\"n\", \"\\\\u\", \"<Cmd>UrlView<CR>\", { desc = \"View buffer URLs\" })\n  vim.keymap.set(\"n\", \"\\\\U\", \"<Cmd>UrlView packer<CR>\", { desc = \"View Packer plugin URLs\" })\n  ```\n\n- You can also hit `:UrlView <tab>` to see additional contexts that you can search from\n  - e.g. `:UrlView packer` to view links for installed [packer.nvim](https://github.com/wbthomason/packer.nvim) plugins\n\n2. You can optionally select a link to bring it up in your browser.\n\n### Buffer URL navigation\n\n1. You can use `[u` and `]u` (default bindings) to jump to the previous and next URL in the buffer respectively.\n2. If desired, you can now press `gx` to open the URL under the cursor in your browser, with netrw.\n3. This keymap can be altered under the `jump` config option.\n\n## 📦 Installation\n\nInstall this plugin with your package manager of choice. You can lazy load this plugin by the `UrlView` command if desired.\n\n- [packer.nvim](https://github.com/wbthomason/packer.nvim)\n\n```lua\nuse(\"axieax/urlview.nvim\")\n```\n\n- [lazy.nvim](https://github.com/folke/lazy.nvim)\n\n```lua\n\"axieax/urlview.nvim\"\n```\n\n## ⚙️ Configuration\n\nThis plugin supports plug-n-play, meaning you can get it up and running without any additional setup.\n\nHowever, you can customise the [default options](lua/urlview/config/default.lua) using the `setup` function:\n\n```lua\nrequire(\"urlview\").setup({\n  -- custom configuration options --\n})\n```\n\nPlease check out the [documentation](doc/urlview.txt) for configuration options and details.\n\n## 🎨 Pickers\n\n### ✔️ Native (vim.ui.select)\n\nYou can customise the appearance of `vim.ui.select` with plugins such as [dressing.nvim](https://github.com/stevearc/dressing.nvim) and [telescope-ui-select.nvim](https://github.com/nvim-telescope/telescope-ui-select.nvim). In the demo images above, I used [dressing.nvim](https://github.com/stevearc/dressing.nvim)'s Telescope option, which allows me to further filter and fuzzy search through my entries.\n\n### 🔭 Telescope\n\n- Optional picker option\n- Additional requirements (only if you're using this picker): [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim)\n- You can use Telescope as your `default_picker` using the `require(\"urlview\").setup` function\n- Alternatively, you can specify a picker dynamically with `:UrlView <ctx> picker=telescope`\n\n### 📑 fzf-lua\n\n- Optional picker option\n- Additional requirements (only if you're using this picker): [fzf-lua](https://github.com/ibhagwan/fzf-lua)\n- You can use fzf-lua as your `default_picker` using the `require(\"urlview\").setup` function\n- Alternatively, you can specify a picker dynamically with `:UrlView <ctx> picker=fzf_lua`\n\n## 🚧 Stay Updated\n\nMore features are continually being added to this plugin (see [🗺️ Roadmap](https://github.com/axieax/urlview.nvim/issues/3)). Feel free to file an issue or create a PR for any features / fixes :)\n\nIt is recommended to subscribe to the [🙉 Breaking Changes](https://github.com/axieax/urlview.nvim/issues/37) thread to be updated on potentially breaking changes to this plugin, as well as resolution strategies.\n"
  },
  {
    "path": "doc/urlview.txt",
    "content": "*urlview.txt*             Find and display URLs from a variety of search contexts\n\n================================================================================\nTable of Contents                                             *urlview.contents*\n\n    INTRODUCTION .......................................... |urlview|\n    CONFIGURATION ......................................... |urlview.config|\n    USAGE ................................................. |urlview.usage|\n    SEARCH ................................................ |urlview.search|\n    PICKERS ............................................... |urlview.pickers|\n    ACTIONS ............................................... |urlview.actions|\n\n\n================================================================================\nINTRODUCTION                                                           *urlview*\n\n✨ urlview.nvim is essentially a plugin which:\n\n1. Finds URLs from a variety of |urlview.search| contexts\n2. Displays these URLs in one of the |urlview.pickers|\n3. Performs |urlview.actions| on selected URLs from the pickers\n\n\n================================================================================\nCONFIGURATION                                                   *urlview.config*\n\nurlview.nvim supports plug-n-play, meaning the default config is automatically\nset up. However, the default options can be configured using the\n`require(\"urlview\").setup` function, with the configuration options below.\n\n                                                  *urlview.config-default_title*\n{default_title}                                       string (default: \"Links:\")\n\nForms part of the prompt title for the picker (`<context> <default_title>`). For\nexample, for the buffer search context with a default_title of \"Links:\", the\npicker title becomes \"Buffer Links:\". The capitalisation of the first letter\nin the default_title determines the capitalisation of the search context in\nthe prompt title as well. For example, a default_title of \"links\" will result\nin a prompt title of \"buffer links\".\n\n                                                 *urlview.config-default_picker*\n{default_picker}                                      string (default: \"native\")\n\nDefault picker for `:UrlView` commands. This should be any one of the pickers in\n|urlview.pickers|.\n\n                                                 *urlview.config-default_prefix*\n{default_prefix}                                    string (default: \"https://\")\n\nDefault prefix for URLs missing a HTTP protocol (e.g. \"www.google.com\" becomes\n\"https://www.google.com\"). Such a protocol is required for |urlview.actions|\nto be able to navigate to URLs. Another suggested option is \"http://\",\nalthough it is less secure than the default HTTPS protocol.\n\n                                                 *urlview.config-default_action*\n{default_action}                                       string (default: \"netrw\")\n\nDefault action to take upon selecting a URL from a picker. This should be any\none of the actions in |urlview.actions|.\n\n                                                 *urlview.config-default_register*\n{default_register}                                     string (default: \"+\")\n\nDefault register to use when yankying.\n\n                                         *urlview.config-default_include_branch*\n{default_include_branch}                                boolean (default: false)\n\nDefault option for whether plugin URLs should link to the branch used by your\npackage manager, for |urlview.search-lazy|, |urlview.search-packer| or\n|urlview.search-vimplug|. When this option is enabled, navigated links will\nopen the plugin's repository to the specific branch specified to your plugin\nmanager.\n\n                                                         *urlview.config-unique*\n{unique}                                                 boolean (default: true)\n\nEnable to ensure links shown in the picker are unique (i.e. no duplicates).\n\n                                                         *urlview.config-sorted*\n{sorted}                                                 boolean (default: true)\n\nEnable to ensure links shown in the picker are sorted alphabetically.\n\n                                                  *urlview.config-log_level_min*\n{log_level_min}    `vim.log.levels` enum or int (default: `vim.log.levels.INFO`)\n\nMinimum log level for output from this plugin. Lower logging levels will be\nignored. >lua\n\n  vim.log.levels = {\n    TRACE = 0,\n    DEBUG = 1,\n    INFO = 2,\n    WARN = 3\n    ERROR = 4,\n    OFF = 5,  -- Neovim v0.8+\n  }\n<\nThe default value of `vim.log.levels.INFO` means that INFO, WARN and ERROR\nlogs will all be displayed. Similarly, a log_level_min value of\n`vim.log.levels.WARN` means that only WARN and ERROR logs will be displayed.\nIt is recommended for this value to be at least `vim.log.levels.WARN` to\nensure warnings are appropriately logged. Setting this value to\n`vim.log.levels.OFF` (requires Neovim 0.8+) or `5` will effectively suppress\nall logs.\n\n                                                           *urlview.config-jump*\n{jump}                                                               table (map)\n\nRegisters keymaps for jumps to the previous or next URL in the buffer.\n\nFields:\n\n    {prev}                                                string (default: \"[u\")\n    Mapping to jump to the previous URL in the buffer. Set to \"\" to disable.\n\n    {next}                                                string (default: \"]u\")\n    Mapping to jump to the previous URL in the buffer. Set to \"\" to disable.\n\n\n================================================================================\nUSAGE                                                            *urlview.usage*\n\nFor normal usage, interaction with this plugin is mostly achieved through the\n`:UrlView` command. This command provides completion to assist with specifying\nappropriate search contexts and respective options.\n\nCalling `:UrlView` without any additional arguments is the same as calling\n`:UrlView buffer`, which finds URLs in the current buffer with the default\noptions configured ( |default_title|, |default_picker|, |default_action|,\n|unique|, |sorted|, etc. ).\n\nThe command expects arguments in the format `:UrlView <ctx> <options...>`,\ne.g. >lua\n\n  :UrlView buffer bufnr=1\n  :UrlView file filepath=/etc/hosts picker=telescope\n  :UrlView packer sorted=false\n<\n\n================================================================================\nSEARCH                                                          *urlview.search*\n\n`urlview.search` provides functions which finds and extracts URLs from a\nparticular search context. The default search contexts can be found by the\nexposed functions in the `urlview.search` module (or `urlview/search/init.lua`).\n\nBuffer Search Context                                    *urlview.search-buffer*\n\nThe buffer search context finds URLs in a specific buffer (current by\ndefault). To search a particular buffer, provide the desired buffer number\nwith `:UrlView buffer bufnr=<bufnr>`.\n\nFile Search Context                                        *urlview.search-file*\n\nThe file search context allows a user to find URLs in a particular file. This\nrequires passing in the parameter `filepath` for URLs in the desired file to be\nsearched, for example with `:UrlView file filepath=\"/etc/hosts\"`.\n\nLazy Search Context                                       *urlview.search-lazy*\n\nThis search context resolves Git repository URLs for plugins installed with\nthe lazy.nvim plugin manager. Invoked with `:UrlView lazy`.\n\nPacker Search Context                                    *urlview.search-packer*\n\nThis search context resolves Git repository URLs for plugins installed with\nthe packer.nvim plugin manager. Invoked with `:UrlView packer`.\n\nvim-plug Search Context                                 *urlview.search-vimplug*\n\nThis search context resolves Git repository URLs for plugins installed with\nthe vim-plug plugin manager. Invoked with `:UrlView vimplug`.\n\nRegistering a custom search context                      *urlview.search-custom*\n\nCustom search contexts can be registered for searching with `:UrlView <ctx>`.\nThe `generate_custom_search` function from the `urlview.search.helpers` module\ncan be used to quickly generate a function for capturing a Lua pattern and\noptionally formatting it with `string.format`. The following resource is very\nhelpful for understanding basic Lua patterns:\n\n- https://riptutorial.com/lua/example/20315/lua-pattern-matching\n\nThis function can then be assigned to the `urlview.search` module for use as a\nsearch context to be selected with the `:UrlView` command (with completion).\n\nHere is an example which finds Jira ticket numbers in the format of \"AXIE-\"\nfollowed by any number (capture field). This entire pattern gets captured and\ncan then be embedded into the format string in the format field, allowing\na user to directly navigate to the Jira ticket in their browser. >lua\n\n  local search = require(\"urlview.search\")\n  local search_helpers = require(\"urlview.search.helpers\")\n  search[\"jira\"] = search_helpers.generate_custom_search({\n    capture = \"AXIE%-%d+\",\n    format = \"https://jira.axieax.com/browse/%s\",\n  })\n\nThis allows for captures such as \"AXIE-1\", \"AXIE-17\", \"AXIE-132\". Feel free to\nadjust the `capture` field to suit your needs.\n\nA custom function can also be registered for a search context, as well as\nadditional parameters with something like >lua\n\n  local search = require(\"urlview.search\")\n  search[\"fruits\"] = function(opts)\n    local fruits = { \"apple\", \"banana\", \"watermelon\" }\n    if opts.include_tomato then\n      table.insert(fruits, \"tomato\")\n    end\n    return fruits\n  end\n\nAdditional parameters can be passed into the custom search context using the\n`:UrlView` command, for example `:UrlView fruits include_tomato` or `:UrlView\nfruits include_tomato=true`. Other types also work.\n\nPlease see `urlview/search/init.lua` for more examples. If you have a useful\ncustom search context, feel free to share it in\nhttps://github.com/axieax/urlview.nvim/discussions/40 for others to use (and\npotentially make it a built-in search context)!\n\n\n================================================================================\nPICKERS                                                        *urlview.pickers*\n\n`urlview.pickers` are used to display the results from |urlview.search|.\n\nvim.ui.select                                           *urlview.pickers-native*\n\nThis picker uses the built-in function `vim.ui.select` to display results. It\nis recommended to use a UI-extension for `vim.ui.select` for enhanced\nfunctionality (similar to |urlview.pickers-telescope|), including customising\npopup locations and behaviour, and searching (even fuzzy-searching) through\nresults. An example of such a plugin is dressing.nvim\n(https://github.com/stevearc/dressing.nvim).\n\ntelescope                                            *urlview.pickers-telescope*\n\nThis picker uses the Telescope plugin\n(https://github.com/nvim-telescope/telescope.nvim) as a picker to display\nresults. Please note that this requires installing Telescope as a plugin in\norder for it to be used as a picker.\n\nRegistering a custom picker                             *urlview.pickers-custom*\n\nA recommendation for registering a custom picker is to first check out a\n`vim.ui.select` UI-extension such as dressing.nvim\n(https://github.com/stevearc/dressing.nvim) to see if your desired picker can\nbe used for the native picker (e.g. nui, fzf).\n\nIn the case that you want to write your own picker, you can register it by\nadding it as a function to the `urlview.pickers` module. >lua\n\n  local pickers = require(\"urlview.pickers\")\n  pickers[\"fzf\"] = function(items, opts)\n    -- TODO\n  end\n<\nPlease check out `urlview/pickers.lua` for examples on how to do this.\n\n\n================================================================================\nACTIONS                                                        *urlview.actions*\n\n`urlview.actions` determine the behaviour when a URL is selected with one of\nthe |urlview.pickers|.\n\nnetrw                                                    *urlview.actions-netrw*\n\nThe `netrw` action uses the built-in netrw feature to open a given URL in your\nbrowser. However, some users may have this feature disabled, either explicitly\nor due to \"netrw hijack\" behaviour from file-explorer Neovim plugins. Due to\nthis, if urlview detects that netrw is disabled, it will use\n|urlview.actions-system| as a fallback by default.\n\nsystem                                                  *urlview.actions-system*\n\nThis action opens URL in your system's default browser depending on your\noperating system. If your system is not supported, please raise a GitHub issue\nto have it included as a built-in options. Otherwise, specify a custom action\n( |urlview.actions-custom|) for your use case.\n\nclipboard                                            *urlview.actions-clipboard*\n\nThis action copies selected URLs to the system clipboard (specifically to\nNeovim's {+} register).\n\nRegistering a custom action                             *urlview.actions-custom*\n\nThere are two main types of custom actions:\n\n1. Execute a shell command which takes in your URL as an argument\n\nThis is the main use case for custom actions - specifying a browser such as\n`chromium` or `firefox` to open your URL with. By default, this executes the\nprovided shell command and passes in the URL as an argument, such as >bash\n\n  $ chromium 'https://www.google.com'\n<\nwhich launches Google in the chromium browser. This can be any executable (be\nsure to find the correct path to your desired application).\n\n2. Lua function\n\nWith urlview.nvim's principle of extensibility in mind, you can also register a\ncustom action as a Lua function by adding it to the `urlview.actions` module,\nlike so: >lua\n\n  local actions = require(\"urlview.actions\")\n  actions[\"spectate\"] = function(raw_url)\n    -- TODO\n  end\n<\n\nPlease refer to `urlview/actions.lua` for examples, and feel free to create a\nGitHub issue or pull request to add your custom Lua function action as a\nbuilt-in action if you think others can use it as well!\n\n\n vim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "lua/urlview/actions.lua",
    "content": "local M = {}\n\nlocal utils = require(\"urlview.utils\")\nlocal config = require(\"urlview.config\")\n\n--- Use command to open the URL\n---@param cmd string @name of executable to run\n---@param args string|table @arg(s) to pass into cmd (unescaped URL string or table of args)\nlocal function shell_exec(cmd, args)\n  if cmd and vim.fn.executable(cmd) == 1 then\n    -- NOTE: `vim.fn.system` shellescapes arguments\n    local cmd_args = { cmd }\n    vim.list_extend(cmd_args, type(args) == \"table\" and args or { args })\n    local err = vim.fn.system(cmd_args)\n    if vim.v.shell_error ~= 0 or err ~= \"\" then\n      utils.log(\n        string.format(\"Failed to navigate link with cmd `%s` and args `%s`\\n%s\", cmd, args, err),\n        vim.log.levels.ERROR\n      )\n    end\n  else\n    utils.log(\n      string.format(\"Cannot use command `%s` to navigate links (either empty or non-executable)\", cmd),\n      vim.log.levels.ERROR\n    )\n  end\nend\n\n--- Use `netrw` to navigate to a URL\n---@param raw_url string @unescaped URL\nfunction M.netrw(raw_url)\n  local url = vim.fn.shellescape(raw_url)\n  local ok, err = pcall(vim.cmd, string.format(\"call netrw#BrowseX(%s, netrw#CheckIfRemote(%s))\", url, url))\n  if not ok and vim.startswith(err, \"Vim(call):E117: Unknown function\") then\n    -- lazily use system action if netrw is disabled\n    M.system(raw_url)\n  end\nend\n\n--- Use the user's default browser to navigate to a URL\n---@param raw_url string @unescaped URL\nfunction M.system(raw_url)\n  local os = utils.os\n  if os == \"Darwin\" then -- MacOS\n    shell_exec(\"open\", raw_url)\n  elseif os == \"Linux\" or os == \"FreeBSD\" then -- Linux and FreeBSD\n    shell_exec(\"xdg-open\", raw_url)\n  elseif os:match(\"Windows\") then -- Windows\n    -- HACK: `start` cmd itself doesn't exist but lives under `cmd`\n    shell_exec(\"cmd\", { \"/C\", \"start\", raw_url })\n  else\n    utils.log(\n      \"Unsupported operating system for `system` action. Please raise a GitHub issue for \" .. os,\n      vim.log.levels.WARN\n    )\n  end\nend\n\n--- Copy URL to clipboard\n---@param raw_url string @unescaped URL\nfunction M.clipboard(raw_url)\n  vim.fn.setreg(config.default_register, raw_url)\n  utils.log(string.format(\"URL %s copied to clipboard\", raw_url), vim.log.levels.INFO)\nend\n\nreturn setmetatable(M, {\n  -- execute action as command if it is not one of the above module keys\n  __index = function(_, k)\n    if k ~= nil then\n      return function(raw_url)\n        return shell_exec(k, raw_url)\n      end\n    end\n  end,\n})\n"
  },
  {
    "path": "lua/urlview/command.lua",
    "content": "local M = {}\n\nlocal utils = require(\"urlview.utils\")\nlocal search_contexts = require(\"urlview.search\")\nlocal search_validation = require(\"urlview.search.validation\")\n\n--- Processes arguments provided through the `UrlView` command for `M.search`\nlocal function command_search(res)\n  local opts = {}\n  local context = res.fargs[1]\n  local option_args = vim.list_slice(res.fargs, 2)\n\n  -- process provided options\n  for _, arg in ipairs(option_args) do\n    local split = vim.split(arg, \"=\", { plain = true })\n    if #split == 1 then\n      opts[arg] = true\n    elseif #split == 2 then\n      local key, value = unpack(split)\n      -- remove beginning and trailing quotes from value if present\n      local inner = value:match([[^['\"](.*)['\"]$]])\n      if inner ~= nil then\n        value = inner\n      end\n      -- type conversion\n      if vim.tbl_contains({ \"true\", \"false\" }, value:lower()) then\n        value = utils.string_to_boolean(value)\n      end\n      opts[key] = value\n    else\n      utils.log(\"Unable to parse argument \" .. arg, vim.log.levels.ERROR)\n      return\n    end\n  end\n\n  require(\"urlview\").search(context, opts)\nend\n\nlocal additional_opts = { \"title\", \"picker\", \"action\", \"sorted\" }\n\nlocal function command_completion(_, line)\n  local args = vim.split(line, \"%s+\")\n  local nargs = #args - 2\n  if nargs == 0 then\n    -- search context completion\n    local contexts = vim.tbl_keys(search_contexts)\n    utils.alphabetical_sort(contexts)\n    return contexts\n  else\n    -- opts completion\n    local context = args[2]\n    local context_opts = search_validation[context]()\n    local accepted_opts = vim.list_extend(context_opts, additional_opts)\n    utils.alphabetical_sort(accepted_opts)\n    return vim.tbl_map(function(value)\n      return value .. \"=\"\n    end, accepted_opts)\n  end\nend\n\nfunction M.register_command()\n  vim.api.nvim_create_user_command(\"UrlView\", command_search, {\n    desc = \"Find URLs in the current buffer or another search context\",\n    complete = command_completion,\n    nargs = \"*\",\n  })\nend\n\nreturn M\n"
  },
  {
    "path": "lua/urlview/config/constants.lua",
    "content": "local constants = {\n  -- SEE: lua pattern matching (https://riptutorial.com/lua/example/20315/lua-pattern-matching)\n  -- regex equivalent: [A-Za-z0-9@:%._+~#=/\\-?&]*\n  pattern = \"[%w@:%%._+~#=/%-?&]*\",\n  http_pattern = \"https?://\",\n  www_pattern = \"www%.\",\n}\n\nreturn constants\n"
  },
  {
    "path": "lua/urlview/config/default.lua",
    "content": "local default_config = {\n  -- Prompt title (`<context> <default_title>`, e.g. `Buffer Links:`)\n  default_title = \"Links:\",\n  -- Default picker to display links with\n  -- Options: \"native\" (vim.ui.select) or \"telescope\"\n  default_picker = \"native\",\n  -- Set the default protocol for us to prefix URLs with if they don't start with http/https\n  default_prefix = \"https://\",\n  -- Command or method to open links with\n  -- Options: \"netrw\", \"system\" (default OS browser), \"clipboard\"; or \"firefox\", \"chromium\" etc.\n  -- By default, this is \"netrw\", or \"system\" if netrw is disabled\n  default_action = \"netrw\",\n  -- Set the register to use when yanking\n  -- Default: + (system clipboard)\n  default_register = \"+\",\n  -- Whether plugin URLs should link to the branch used by your package manager\n  default_include_branch = false,\n  -- Ensure links shown in the picker are unique (no duplicates)\n  unique = true,\n  -- Ensure links shown in the picker are sorted alphabetically\n  sorted = true,\n  -- Minimum log level (recommended at least `vim.log.levels.WARN` for error detection warnings)\n  log_level_min = vim.log.levels.INFO,\n  -- Keymaps for jumping to previous / next URL in buffer\n  jump = {\n    prev = \"[u\",\n    next = \"]u\",\n  },\n}\n\nreturn default_config\n"
  },
  {
    "path": "lua/urlview/config/helpers.lua",
    "content": "local M = {}\n\nlocal config = require(\"urlview.config\")\nlocal default_config = require(\"urlview.config.default\")\n\nfunction M.reset_defaults()\n  config._options = default_config\nend\n\nfunction M.update_config(user_config)\n  config._options = vim.tbl_deep_extend(\"force\", config._options, user_config)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/urlview/config/init.lua",
    "content": "local M = {\n  _options = {},\n}\n\nreturn setmetatable(M, {\n  -- general get and set operations refer to the internal table `_options`\n  __index = function(_, k)\n    return M._options[k]\n  end,\n  __newindex = function(_, k, v)\n    M._options[k] = v\n  end,\n})\n"
  },
  {
    "path": "lua/urlview/init.lua",
    "content": "local M = {}\n\nlocal actions = require(\"urlview.actions\")\nlocal command = require(\"urlview.command\")\nlocal config = require(\"urlview.config\")\nlocal config_helpers = require(\"urlview.config.helpers\")\nlocal jump = require(\"urlview.jump\")\nlocal search = require(\"urlview.search\")\nlocal search_validation = require(\"urlview.search.validation\")\nlocal pickers = require(\"urlview.pickers\")\nlocal utils = require(\"urlview.utils\")\n\n--- Searchs the provided context for links\n---@param ctx string where to search (default: search buffer)\n---@param opts table (map, optional)\nfunction M.search(ctx, opts)\n  ctx = utils.fallback(ctx, \"buffer\")\n  opts = utils.fallback(opts, {})\n  opts.action = utils.fallback(opts.action, config.default_action)\n  local picker = utils.fallback(opts.picker, config.default_picker)\n  if not opts.title then\n    local should_capitalise = string.match(config.default_title, \"^%u\")\n    local ctx_title = utils.ternary(should_capitalise, ctx:gsub(\"^%l\", string.upper), ctx)\n    opts.title = string.format(\"%s %s\", ctx_title, config.default_title)\n  end\n\n  -- search ctx for links and display with picker\n  opts = search_validation[ctx](opts)\n  local links = search[ctx](opts)\n  links = utils.process_links(links, opts)\n  if links and not vim.tbl_isempty(links) then\n    if type(opts.action) == \"string\" then\n      opts.action = actions[opts.action]\n    end\n    pickers[picker](links, opts)\n  else\n    utils.log(\"No links found in context \" .. ctx, vim.log.levels.INFO)\n  end\nend\n\nlocal function autoload()\n  config_helpers.reset_defaults()\n  command.register_command()\nend\n\nautoload()\n\n--- Custom setup function\n--- Not required to be called unless user wants to modify the default config\n---@param user_config table (optional)\nfunction M.setup(user_config)\n  user_config = utils.fallback(user_config, {})\n  config_helpers.update_config(user_config)\n\n  jump.register_mappings(config.jump)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/urlview/jump.lua",
    "content": "local M = {}\n\n-- NOTE: line numbers are 1-indexed, column numbers are 0-indexed\n\nlocal utils = require(\"urlview.utils\")\nlocal search_helpers = require(\"urlview.search.helpers\")\n\nlocal END_COL = -1\n\n--- Return the starting positions of `match` in `line`\n---@param line string\n---@param match string\n---@param offset number @added to each position\n---@return table (list) of offsetted starting indicies\nfunction M.line_match_positions(line, match, offset)\n  local res = {}\n  local init = 1\n  while init <= #line do\n    local start, finish = line:find(match, init, true)\n    if start == nil then\n      return res\n    end\n\n    table.insert(res, start + offset - 1)\n    init = finish\n  end\n\n  return res\nend\n\n--- Returns a starting column position not on a URL\n---@param line_start number @line number at cursor\n---@param col_start number @column number at cursor\n---@param reversed boolean @direction\n---@return number @corrected starting column\nfunction M.correct_start_col(line_start, col_start, reversed)\n  local full_line = vim.fn.getline(line_start)\n  local matches = search_helpers.content(full_line)\n  for _, match in ipairs(matches) do\n    local positions = M.line_match_positions(full_line, match, 0)\n    for _, position in ipairs(positions) do\n      local url_end = position + #match\n      local on_url = col_start >= position and col_start < url_end\n      -- edge case for going backwards with cursor at start of URL\n      if on_url and reversed and position == col_start then\n        return math.max(col_start - 1, 0)\n      -- generally if on a URL, move column to be after the URL\n      elseif on_url then\n        return url_end\n      end\n    end\n  end\n  return col_start\nend\n\nlocal reversed_sort_function_lookup = {\n  -- reversed == true: descending sort\n  [true] = function(a, b)\n    return a > b\n  end,\n  -- reversed == false: ascending sort\n  [false] = function(a, b)\n    return a < b\n  end,\n}\n\n--- Finds the position of the previous / next URL\n---@param winnr number @id of current window\n---@param reversed boolean @direction false for forward, true for backwards\n---@return table|nil @position\nfunction M.find_url(winnr, reversed)\n  local line_no, col_no = unpack(vim.api.nvim_win_get_cursor(winnr))\n  local total_lines = vim.api.nvim_buf_line_count(0)\n  col_no = M.correct_start_col(line_no, col_no, reversed)\n\n  local sort_function = reversed_sort_function_lookup[reversed]\n  local line_last = utils.ternary(reversed, 0, total_lines + 1)\n  while line_no ~= line_last do\n    local full_line = vim.fn.getline(line_no)\n    col_no = utils.ternary(col_no == END_COL, #full_line, col_no)\n    local line = utils.ternary(reversed, full_line:sub(1, col_no), full_line:sub(col_no + 1))\n    local matches = search_helpers.content(line)\n\n    if not vim.tbl_isempty(matches) then\n      -- sorted table(list) of starting column numbers for URLs in line\n      -- normal order: ascending, reversed order: descending\n      local indices = {}\n      for _, match in ipairs(matches) do\n        local offset = utils.ternary(reversed, 0, col_no)\n        vim.list_extend(indices, M.line_match_positions(line, match, offset))\n      end\n      table.sort(indices, sort_function)\n      -- find first valid (before or after current column)\n      for _, index in ipairs(indices) do\n        local valid = utils.ternary(reversed, index <= col_no, index >= col_no)\n        if valid then\n          return { line_no, index }\n        end\n      end\n    end\n\n    line_no = utils.ternary(reversed, line_no - 1, line_no + 1)\n    col_no = utils.ternary(reversed, END_COL, 0)\n  end\nend\n\n--- Forward / backward jump generator\n---@param reversed boolean @direction false for forward, true for backwards\n---@return function @when called, jumps to the URL in the given direction\nlocal function goto_url(reversed)\n  return function()\n    local direction = utils.ternary(reversed, \"previous\", \"next\")\n    local winnr = vim.api.nvim_get_current_win()\n    local pos = M.find_url(winnr, reversed)\n    if not pos then\n      utils.log(string.format(\"Cannot find any %s URLs in buffer\", direction), vim.log.levels.INFO)\n      return\n    end\n\n    if vim.api.nvim_win_is_valid(winnr) then\n      vim.cmd(\"normal! m'\") -- add to jump list\n      vim.api.nvim_win_set_cursor(winnr, pos)\n    else\n      utils.log(\n        string.format(\"The %s URL was found in window number %s, which is no longer valid\", direction, winnr),\n        vim.log.levels.WARN\n      )\n    end\n  end\nend\n\n--- Jump to the next URL\nM.next_url = goto_url(false)\n\n--- Jump to the previous URL\nM.prev_url = goto_url(true)\n\n--- Register URL jump mappings\n---@param jump_opts table\nfunction M.register_mappings(jump_opts)\n  if type(jump_opts) ~= \"table\" then\n    utils.log(\n      \"Invalid type for option `jump` (expected: table with `prev` and `next` key mappings)\",\n      vim.log.levels.WARN\n    )\n  else\n    if jump_opts.prev ~= \"\" then\n      vim.keymap.set(\"n\", jump_opts.prev, function()\n        require(\"urlview.jump\").prev_url()\n      end, { desc = \"Previous URL\" })\n    end\n    if jump_opts.next ~= \"\" then\n      vim.keymap.set(\"n\", jump_opts.next, function()\n        require(\"urlview.jump\").next_url()\n      end, { desc = \"Next URL\" })\n    end\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/urlview/pickers.lua",
    "content": "local M = {}\n\nlocal utils = require(\"urlview.utils\")\n\n--- Displays items using the vim.ui.select picker\n---@param items table (list) of strings\n---@param opts table (map) of options\nfunction M.native(items, opts)\n  local options = { prompt = opts.title }\n  local function on_choice(item, _)\n    if item then\n      opts.action(item)\n    end\n  end\n\n  vim.ui.select(items, options, on_choice)\nend\n\n--- Displays items using the fzf-lua picker\n---@param items table (list) of strings\n---@param opts table (map) of options\nfunction M.fzf_lua(items, opts)\n  local fzf = pcall(require, \"fzf-lua\")\n  if not fzf then\n    utils.log(\"fzf-lua is not installed, defaulting to native vim.ui.select picker.\", vim.log.levels.INFO)\n    return M.native(items, opts)\n  end\n\n  require(\"fzf-lua\").fzf_exec(items, {\n    prompt = opts.title,\n    fzf_opts = { [\"--multi\"] = true },\n    actions = {\n      [\"default\"] = function(selected)\n        for _, entry in ipairs(selected) do\n          opts.action(entry)\n        end\n      end,\n    },\n  })\nend\n\n--- Displays items using the Telescope picker\n---@param items table (list) of strings\n---@param opts table (map) of options\nfunction M.telescope(items, opts)\n  local telescope = pcall(require, \"telescope\")\n  if not telescope then\n    utils.log(\"Telescope is not installed, defaulting to native vim.ui.select picker.\", vim.log.levels.INFO)\n    return M.native(items, opts)\n  end\n\n  local actions = require(\"telescope.actions\")\n  local action_state = require(\"telescope.actions.state\")\n  local conf = require(\"telescope.config\").values\n  local finders = require(\"telescope.finders\")\n  local pickers = require(\"telescope.pickers\")\n\n  pickers\n    .new(opts, {\n      prompt_title = opts.title,\n      finder = finders.new_table({\n        results = items,\n      }),\n      sorter = conf.generic_sorter(opts),\n      attach_mappings = function(prompt_bufnr, _)\n        actions.select_default:replace(function()\n          local picker = action_state.get_current_picker(prompt_bufnr)\n          local multi = picker:get_multi_selection()\n          local single = picker:get_selection()\n          actions.close(prompt_bufnr)\n          if #multi > 0 then\n            for _, entry in ipairs(multi) do\n              opts.action(entry[1])\n            end\n          elseif single[1] then\n            opts.action(single[1])\n          end\n        end)\n        return true\n      end,\n    })\n    :find()\nend\n\nreturn setmetatable(M, {\n  -- use default `vim.ui.select` when provided an invalid picker\n  __index = function(_, k)\n    if k ~= nil then\n      utils.log(k .. \" is not a valid picker, defaulting to native vim.ui.select picker.\", vim.log.levels.INFO)\n      return M.native\n    end\n  end,\n})\n"
  },
  {
    "path": "lua/urlview/search/helpers.lua",
    "content": "local M = {}\n\nlocal constants = require(\"urlview.config.constants\")\nlocal utils = require(\"urlview.utils\")\n\n--- Extracts content from a given buffer\n---@param bufnr number (optional)\n---@return string @content of buffer\nfunction M.get_buffer_content(bufnr)\n  bufnr = utils.fallback(bufnr, 0)\n  if not vim.api.nvim_buf_is_valid(bufnr) then\n    utils.log(string.format(\"Invalid buffer number provided: %s\", bufnr), vim.log.levels.ERROR)\n    return \"\"\n  end\n  return table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), \"\\n\")\nend\n\n--- Extracts content from a given file\n---@param filepath string @path to file\n---@return string @content of file (or empty string if file cannot be open)\nfunction M.read_file(filepath)\n  local f, err = io.open(vim.fn.expand(filepath), \"r\")\n  if f == nil then\n    utils.log(err, vim.log.levels.ERROR)\n    return \"\"\n  end\n  local content = f:read(\"*all\")\n  f:close()\n  return content\nend\n\n--- Extracts urls from the given content\n---@param content string\n---@return table (list) of strings (extracted links)\nfunction M.content(content)\n  ---@type table (map) of string (url base pattern) to string (prefix / uri protocol)\n  local captures = {}\n\n  -- NOTE: this method enforces unique matches regardless of config (before a general pattern is implemented)\n\n  -- Extract URLs starting with http:// or https://\n  for capture in content:gmatch(constants.http_pattern .. \"%w\" .. constants.pattern) do\n    local prefix = capture:match(constants.http_pattern)\n    local url = capture:gsub(constants.http_pattern, \"\")\n    captures[url] = prefix\n  end\n\n  -- Extract URLs starting with www, excluding already extracted http(s) URLs\n  for capture in content:gmatch(constants.www_pattern .. \"%w\" .. constants.pattern) do\n    if not captures[capture] then\n      captures[capture] = \"\"\n    end\n  end\n\n  -- Combine captures\n  local links = {}\n  for url, prefix in pairs(captures) do\n    local link = prefix .. url\n    if link ~= \"\" then\n      table.insert(links, link)\n    end\n  end\n\n  return links\nend\n\n--- Extract @captures from @content and display them as @formats\n---@param content string @content to extract from\n---@param capture string @capture pattern to extract\n---@param format string @Optional format pattern to display\n---@return table @list of extracted links\nfunction M.extract_pattern(content, capture, format)\n  local captures = {}\n  for c in content:gmatch(capture) do\n    local fmt = format and string.format(format, c) or c\n    table.insert(captures, fmt)\n  end\n  return captures\nend\n\n--- Extract Git links from @plugins_spec\n---@param plugins_spec table @map of specs for plugins\n---@param uri_key string @key in plugin_spec containing the Git URL\n---@param include_branch boolean @whether to include the branch in the URL\n---@return table @list of extracted links\nfunction M.extract_plugins_spec(plugins_spec, uri_key, include_branch)\n  local function filter_files(plugin_url)\n    if plugin_url == nil then\n      return false\n    end\n    local fs_stat = vim.loop.fs_stat(plugin_url)\n    return not fs_stat or vim.tbl_isempty(fs_stat)\n  end\n\n  local function extract_key(plugin_spec)\n    if plugin_spec[uri_key] == nil then\n      return nil\n    end\n    local uri = plugin_spec[uri_key]:gsub(\"%.git$\", \"\") -- remove `.git` suffix\n    if include_branch and plugin_spec.branch then\n      uri = string.format(\"%s/tree/%s\", uri, plugin_spec.branch)\n    end\n    return uri\n  end\n\n  local plugins = vim.tbl_map(extract_key, vim.tbl_values(plugins_spec or {}))\n  return vim.tbl_filter(filter_files, plugins)\nend\n\n--- Generates a simple search function from a template table\n---@param pattern table (map) with `capture` key and optional `format` key\n---@return function|nil\nfunction M.generate_custom_search(pattern)\n  if not pattern.capture then\n    utils.log(\"Unable to generate custom search: please ensure that the table has 'capture' field\", vim.log.levels.WARN)\n    return nil\n  end\n\n  return function(opts)\n    local content = opts.content or M.get_buffer_content(opts.bufnr)\n    return M.extract_pattern(content, pattern.capture, pattern.format)\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/urlview/search/init.lua",
    "content": "local M = {}\n\nlocal utils = require(\"urlview.utils\")\nlocal search_helpers = require(\"urlview.search.helpers\")\n\n-- NOTE: make sure to add accepted params for `opts` to `urlview.search.validation` as well if needed\n\n--- Extracts urls from the current buffer or a given buffer\n---@param opts table (map)\n---@return table (list) of strings (extracted links)\nfunction M.buffer(opts)\n  local content = search_helpers.get_buffer_content(opts.bufnr)\n  return search_helpers.content(content)\nend\n\n--- Extracts urls from a given file\n---@param opts table (map)\n---@return table (list) of strings (extracted links)\nfunction M.file(opts)\n  local content = search_helpers.read_file(opts.filepath)\n  return search_helpers.content(content)\nend\n\n--- Extracts urls of packer.nvim plugins\n---@param opts table (map)\n---@return table (list) of strings (extracted links)\nfunction M.packer(opts)\n  -- selene: allow(global_usage)\n  local plugins = _G.packer_plugins or {}\n  return search_helpers.extract_plugins_spec(plugins, \"url\", opts.include_branch)\nend\n\n--- Extracts urls of lazy.nvim plugins\n---@param opts table (map)\n---@return table (list) of strings (extracted links)\nfunction M.lazy(opts)\n  local ok, lazy = pcall(require, \"lazy\")\n  local plugins = ok and lazy.plugins() or {}\n  return search_helpers.extract_plugins_spec(plugins, \"url\", opts.include_branch)\nend\n\n--- Extracts urls of vim-plug plugins\n---@param opts table (map)\n---@return table (list) of strings (extracted links)\nfunction M.vimplug(opts)\n  local plugins = vim.g.plugs or {}\n  return search_helpers.extract_plugins_spec(plugins, \"uri\", opts.include_branch)\nend\n\nreturn setmetatable(M, {\n  -- error check for invalid searcher (still allow function calls, but return empty table)\n  __index = function(_, k)\n    if k ~= nil then\n      utils.log(\"Cannot search context \" .. k, vim.log.levels.WARN)\n      return function()\n        return {}\n      end\n    end\n  end,\n})\n"
  },
  {
    "path": "lua/urlview/search/validation.lua",
    "content": "local M = {}\n\nlocal utils = require(\"urlview.utils\")\nlocal config = require(\"urlview.config\")\n\n--- Validates `opts` and sets default values if `opts` is provided, otherwise returns all possible accepted options\n---@param opts table (map) of user options\n---@param rules table (map) of vim.validate rules (with an optional fourth tuple member for default value)\n---       (option_key (string) -> { type (string), optional: boolean, default: any })\n---@return table (map) of updated user options if `opts` is provided, otherwise returns all possible accepted options as a table (list)\nlocal function verify_or_accept(opts, rules)\n  if not opts then\n    -- return valid options if `opts` not provided\n    return vim.tbl_keys(rules)\n  end\n\n  local new_opts = vim.deepcopy(opts)\n  for key, value in pairs(rules) do\n    if #value == 4 and value[3] and opts[key] == nil then\n      local default = value[4]\n      new_opts[key] = default\n      rules[key][1] = default\n    end\n  end\n\n  vim.validate(rules)\n  return new_opts\nend\n\n-- NOTE: validation for `urlview.search.init` functions go below\n\n--- Validation for the \"buffer\" search context\n---@param opts table (map, optional) of user options\n---@return table (map) of updated user options if `opts` is provided, otherwise returns all possible accepted options as a table (list)\nfunction M.buffer(opts)\n  return verify_or_accept(opts, {\n    bufnr = { opts.bufnr, \"number\", true, 0 },\n  })\nend\n\n--- Validation for the \"file\" search context\n---@param opts table (map, optional) of user options\n---@return table (map) of updated user options if `opts` is provided, otherwise returns all possible accepted options as a table (list)\nfunction M.file(opts)\n  return verify_or_accept(opts, {\n    filepath = { opts.filepath, \"string\", false },\n  })\nend\n\n--- Validation for the \"packer\" search context\n---@param opts table (map, optional) of user options\n---@return table (map) of updated user options if `opts` is provided, otherwise returns all possible accepted options as a table (list)\nfunction M.packer(opts)\n  return verify_or_accept(opts, {\n    include_branch = { opts.include_branch, \"boolean\", true, config.default_include_branch },\n  })\nend\n\n--- Validation for the \"lazy\" search context\n---@param opts table (map, optional) of user options\n---@return table (map) of updated user options if `opts` is provided, otherwise returns all possible accepted options as a table (list)\nfunction M.lazy(opts)\n  return verify_or_accept(opts, {\n    include_branch = { opts.include_branch, \"boolean\", true, config.default_include_branch },\n  })\nend\n\n--- Validation for the \"vimplug\" search context\n---@param opts table (map, optional) of user options\n---@return table (map) of updated user options if `opts` is provided, otherwise returns all possible accepted options as a table (list)\nfunction M.vimplug(opts)\n  return verify_or_accept(opts, {\n    include_branch = { opts.include_branch, \"boolean\", true, config.default_include_branch },\n  })\nend\n\nreturn setmetatable(M, {\n  __index = function(_, _)\n    return function(opts)\n      -- if `opts` provided, return `opts`, otherwise return all possible accepted options\n      return utils.fallback(opts, {})\n    end\n  end,\n})\n"
  },
  {
    "path": "lua/urlview/utils.lua",
    "content": "local M = {}\n\nlocal config = require(\"urlview.config\")\nlocal constants = require(\"urlview.config.constants\")\n\nM.os = vim.loop.os_uname().sysname\n\nfunction M.alphabetical_sort(tbl)\n  table.sort(tbl, function(a, b)\n    return a:lower() < b:lower()\n  end)\nend\n\n--- Processes links before being displayed\n---@param links table @list of extracted links\n---@param opts table @Optional options\n---@return table @list of prepared links\nfunction M.process_links(links, opts)\n  opts = M.fallback(opts, {})\n  local new_links = {}\n\n  -- Attach missing HTTP(s) protocol\n  for _, link in ipairs(links) do\n    if not link:match(\"^\" .. constants.http_pattern) then\n      link = config.default_prefix .. link\n    end\n    table.insert(new_links, link)\n  end\n\n  -- Filter duplicate links\n  -- NOTE: links with different protocols / www prefix / trailing slashes are not filtered to ensure links do not break\n  if M.fallback(opts.unique, config.unique) then\n    local map = {}\n    for _, link in ipairs(new_links) do\n      map[link] = true\n    end\n    new_links = vim.tbl_keys(map)\n  end\n\n  -- Sort links alphabetically (case insensitive)\n  if M.fallback(opts.sorted, config.sorted) then\n    M.alphabetical_sort(new_links)\n  end\n\n  return new_links\nend\n\n--- Determines whether to accept the current value or use a fallback value\n---@param value any @value to check\n---@param fallback_value any @fallback value to use\n---@param fallback_comparison any @fallback comparison, defaults to nil\n---@return any @value, or @fallback if @value is @fallback_comparison\nfunction M.fallback(value, fallback_value, fallback_comparison)\n  return (value == fallback_comparison and fallback_value) or value\nend\n\n--- Mimics the ternary operator\n---@param condition boolean @condition to check\n---@param if_true any @value to return if @condition is true\n---@param if_false any @value to return if @condition is false\n---@return any @condition ? if_true : if_false\nfunction M.ternary(condition, if_true, if_false)\n  return (condition and if_true) or if_false\nend\n\n--- Logs user warnings\n---@param message string @message to log\n---@param level integer|nil @log level, defaults to \"warning\"\nfunction M.log(message, level)\n  level = M.fallback(level, vim.log.levels.WARN)\n  if level >= config.log_level_min then\n    vim.notify(\"[urlview.nvim] \" .. message, level)\n  end\nend\n\n--- Converts a boolean to a string\n---@param value string @value to convert\n---@return boolean|nil @value as a boolean, or nil if not a boolean\nfunction M.string_to_boolean(value)\n  value = value:lower()\n  local bool_map = { [\"true\"] = true, [\"false\"] = false }\n  if not bool_map[value] then\n    M.log(\"Could not convert \" .. value .. \" to boolean\")\n  end\n  return bool_map[value]\nend\n\nreturn M\n"
  },
  {
    "path": "selene.toml",
    "content": "std = \"vim\"\n"
  },
  {
    "path": "stylua.toml",
    "content": "column_width = 120\nline_endings = \"Unix\"\nindent_type = \"Spaces\"\nindent_width = 2\nquote_style = \"AutoPreferDouble\"\nno_call_parentheses = false\n"
  },
  {
    "path": "tests/init.vim",
    "content": "set noswapfile\nset rtp+=../plenary.nvim\nset rtp+=../urlview.nvim\nruntime! plugin/plenary.vim\n"
  },
  {
    "path": "tests/urlview/capture_custom_searches_spec.lua",
    "content": "local search = require(\"urlview.search\")\nlocal search_helpers = require(\"urlview.search.helpers\")\nlocal assert_tbl_same_any_order = require(\"tests.urlview.helpers\").assert_tbl_same_any_order\n\ndescribe(\"custom Jira searcher (template table)\", function()\n  before_each(function()\n    search.jira = search_helpers.generate_custom_search({\n      capture = \"AXIE%-%d+\",\n      format = \"https://jira.axieax.com/browse/%s\",\n    })\n    assert.is_not.Nil(search.jira)\n  end)\n\n  after_each(function()\n    search.jira = nil\n  end)\n\n  it(\"capture single\", function()\n    local content = \"AXIE-1\"\n    local links = search.jira({ content = content })\n    assert_tbl_same_any_order({\n      \"https://jira.axieax.com/browse/AXIE-1\",\n    }, links)\n  end)\n\n  it(\"capture multiple\", function()\n    local content = [[\n      AXIE-1\n      AXIE-12\n      AXIE-123\n      AXIE-1234\n      AXIE-12345\n      AXIE-123456\n      AXIE-1234567\n      AXIE-12345678\n      AXIE-123456789\n      AXIE-1234567890\n    ]]\n    local links = search.jira({ content = content })\n    assert_tbl_same_any_order({\n      \"https://jira.axieax.com/browse/AXIE-1\",\n      \"https://jira.axieax.com/browse/AXIE-12\",\n      \"https://jira.axieax.com/browse/AXIE-123\",\n      \"https://jira.axieax.com/browse/AXIE-1234\",\n      \"https://jira.axieax.com/browse/AXIE-12345\",\n      \"https://jira.axieax.com/browse/AXIE-123456\",\n      \"https://jira.axieax.com/browse/AXIE-1234567\",\n      \"https://jira.axieax.com/browse/AXIE-12345678\",\n      \"https://jira.axieax.com/browse/AXIE-123456789\",\n      \"https://jira.axieax.com/browse/AXIE-1234567890\",\n    }, links)\n  end)\n\n  it(\"invalid captures ignored\", function()\n    local content = [[\n    AXIE\n    AXIE1\n    AXIE--123\n    AXIE%-1\n    AXIE-!\n    AXIE-AXIE\n    AXIE-ax\n    ]]\n    local links = search.jira({ content = content })\n    assert_tbl_same_any_order({}, links)\n  end)\nend)\n\ndescribe(\"overwrite default searcher\", function()\n  local builtin_search_buffer = search.buffer\n\n  before_each(function()\n    search.buffer = search_helpers.generate_custom_search({\n      capture = \"l.ve\",\n      format = \"i-%s-testing\",\n    })\n  end)\n\n  after_each(function()\n    search.buffer = builtin_search_buffer\n  end)\n\n  it(\"capture single\", function()\n    local content = \"i love testing\"\n    local result = search.buffer({ content = content })\n    assert_tbl_same_any_order({\n      \"i-love-testing\",\n    }, result)\n  end)\n\n  it(\"capture multiple\", function()\n    local content = \"I live to see another day, I love Neovim\"\n    local result = search.buffer({ content = content })\n    assert_tbl_same_any_order({\n      \"i-love-testing\",\n      \"i-live-testing\",\n    }, result)\n  end)\nend)\n\ndescribe(\"custom function\", function()\n  before_each(function()\n    search.test = function(opts)\n      return { opts.a or \"default\", opts.b or \"default\", opts.c or \"default\" }\n    end\n  end)\n\n  after_each(function()\n    search.test = nil\n  end)\n\n  it(\"capture one\", function()\n    local result = search.test({ a = \"a\" })\n    assert_tbl_same_any_order({ \"a\", \"default\", \"default\" }, result)\n  end)\n\n  it(\"capture two\", function()\n    local result = search.test({ a = \"a\", c = \"c\" })\n    assert_tbl_same_any_order({ \"a\", \"c\", \"default\" }, result)\n  end)\n\n  it(\"capture three\", function()\n    local result = search.test({ a = \"a\", b = \"b\", c = \"c\" })\n    assert_tbl_same_any_order({ \"a\", \"b\", \"c\" }, result)\n  end)\n\n  it(\"ignore extra\", function()\n    local result = search.test({ a = \"a\", b = \"b\", c = \"c\", d = \"d\" })\n    assert_tbl_same_any_order({ \"a\", \"b\", \"c\" }, result)\n  end)\nend)\n\ndescribe(\"register custom search\", function()\n  it(\"captures git uris\", function()\n    search.git = search_helpers.generate_custom_search({\n      capture = \"git@[^%s]+%.git\",\n    })\n\n    local content = [[\n      git@github.com:axieax/urlview.nvim.git\n      git@github.com:axieax/typo.nvim.git\n    ]]\n\n    local links = search.git({ content = content })\n    assert_tbl_same_any_order({\n      \"git@github.com:axieax/urlview.nvim.git\",\n      \"git@github.com:axieax/typo.nvim.git\",\n    }, links)\n    search.git = nil\n  end)\n\n  it(\"captures ssh uris iwthout the prefix\", function()\n    search.ssh = search_helpers.generate_custom_search({\n      capture = \"ssh://([^%s]+)\",\n    })\n\n    local content = [[\n      ssh://git@github.com:axieax/urlview.nvim.git\n      ssh://git@github.com:axieax/typo.nvim.git\n      ssh://192.168.1.1\n      ssh://192.168.1.1:4000\n    ]]\n\n    local links = search.ssh({ content = content })\n    assert_tbl_same_any_order({\n      \"git@github.com:axieax/urlview.nvim.git\",\n      \"git@github.com:axieax/typo.nvim.git\",\n      \"192.168.1.1\",\n      \"192.168.1.1:4000\",\n    }, links)\n    search.ssh = nil\n  end)\n\n  it(\"captures ftp uris\", function()\n    search.ftp = search_helpers.generate_custom_search({\n      capture = \"ftp://[^%s]+\",\n    })\n\n    local content = [[\n      ftp://ftp.example.com\n      ftp://user@host/%2Ffoo/bar.txt\n      ftp://user:password@server/pathname;type=a\n    ]]\n\n    local links = search.ftp({ content = content })\n    assert_tbl_same_any_order({\n      \"ftp://ftp.example.com\",\n      \"ftp://user@host/%2Ffoo/bar.txt\",\n      \"ftp://user:password@server/pathname;type=a\",\n    }, links)\n    search.ftp = nil\n  end)\nend)\n"
  },
  {
    "path": "tests/urlview/capture_jump_spec.lua",
    "content": "local jump = require(\"urlview.jump\")\nlocal jump_helpers = require(\"tests.urlview.jump_helpers\")\nlocal set_cursor = jump_helpers.set_cursor\nlocal create_buffer = jump_helpers.create_buffer\nlocal teardown_windows = jump_helpers.teardown_windows\nlocal jump_forwards = jump_helpers.jump_forwards\nlocal jump_backwards = jump_helpers.jump_backwards\n\n-- ASSUMPTION(cursor_pos): line numbers are 1-indexed, column numbers are 0-indexed\n\nlocal examples = {\n  empty = \"\",\n  invalid = \"hello\",\n  single_line_middle = \"abc https://www.google.com def\",\n  standard_url = \"https://www.google.com\",\n  multi_line_just_links = [[\nhttps://www.google.com\nhttps://www.github.com\nhttps://www.amazon.com\nhttps://www.reddit.com]],\n  multi_line_sandwich = [[\n\nhttps://www.google.com\n]],\n}\n\ndescribe(\"line_match_positions unit tests\", function()\n  it(\"multiple substrings\", function()\n    local line = \"abc abc abc\"\n    local expected_no_offset = { 0, 4, 8 }\n    for offset = 0, 100 do\n      local res = jump.line_match_positions(line, \"abc\", offset)\n      local expected = vim.tbl_map(function(x)\n        return x + offset\n      end, expected_no_offset)\n      vim.deep_equal(expected, res)\n    end\n  end)\n\n  it(\"single URL\", function()\n    local url = examples.standard_url\n    local res = jump.line_match_positions(url, url, 0)\n    vim.deep_equal({ 0 }, res)\n  end)\n\n  it(\"correct single index\", function()\n    local url = examples.standard_url\n    local line = examples.single_line_middle\n    local res = jump.line_match_positions(line, url, 0)\n    vim.deep_equal({ 4 }, res)\n  end)\nend)\n\ndescribe(\"correct starting column\", function()\n  it(\"backwards no URL\", function()\n    local line = examples.invalid\n    create_buffer(line)\n    for col = 0, #line do\n      local new_col = jump.correct_start_col(1, col, true)\n      assert.equals(col, new_col)\n    end\n  end)\n\n  it(\"backwards before URL\", function()\n    local line = examples.single_line_middle\n    create_buffer(line)\n    local url_start = 4\n    for col = 0, url_start - 1 do\n      local new_col = jump.correct_start_col(1, col, true)\n      assert.equals(col, new_col)\n    end\n  end)\n\n  it(\"backwards on start of URL\", function()\n    local line = examples.single_line_middle\n    local url_start = 4\n    create_buffer(line)\n    local new_col = jump.correct_start_col(1, url_start, true)\n    assert.equals(url_start - 1, new_col)\n  end)\n\n  it(\"backwards on rest of URL\", function()\n    local line = examples.single_line_middle\n    create_buffer(line)\n    local url_start = 4\n    local url_end = url_start + #examples.standard_url\n    for col = url_start + 1, url_end - 1 do\n      local new_col = jump.correct_start_col(1, col, true)\n      assert.equals(url_end, new_col)\n    end\n  end)\n\n  it(\"backwards after URL\", function()\n    local line = examples.single_line_middle\n    local url_start = 4\n    local url_end = url_start + #examples.standard_url\n    for col = url_end, #line do\n      local new_col = jump.correct_start_col(1, col, true)\n      assert.equals(col, new_col)\n    end\n  end)\n\n  it(\"forwards no URL\", function()\n    local line = examples.invalid\n    create_buffer(line)\n    for col = 0, #line do\n      local new_col = jump.correct_start_col(1, col, false)\n      assert.equals(col, new_col)\n    end\n  end)\n\n  it(\"forwards before URL\", function()\n    local line = examples.single_line_middle\n    create_buffer(line)\n    local url_start = 4\n    for col = 0, url_start - 1 do\n      local new_col = jump.correct_start_col(1, col, false)\n      assert.equals(col, new_col)\n    end\n  end)\n\n  it(\"forwards on URL\", function()\n    local line = examples.single_line_middle\n    create_buffer(line)\n    local url_start = 4\n    local url_end = url_start + #examples.standard_url\n    for col = url_start, url_end - 1 do\n      local new_col = jump.correct_start_col(1, col, false)\n      assert.equals(url_end, new_col)\n    end\n  end)\n\n  it(\"forwards after URL\", function()\n    local line = examples.single_line_middle\n    local url_start = 4\n    local url_end = url_start + #examples.standard_url\n    for col = url_end, #line do\n      local new_col = jump.correct_start_col(1, col, false)\n      assert.equals(col, new_col)\n    end\n  end)\nend)\n\ndescribe(\"backwards jump\", function()\n  after_each(teardown_windows)\n\n  it(\"empty buffer\", function()\n    local content = examples.empty\n    create_buffer(content, { 1, 0 })\n    local res = jump_backwards()\n    assert.is_nil(res)\n  end)\n\n  it(\"no URL\", function()\n    local content = examples.invalid\n    create_buffer(content)\n    for col = 0, #content do\n      set_cursor({ 1, col })\n      local res = jump_backwards()\n      assert.is_nil(res)\n    end\n  end)\n\n  it(\"just URL\", function()\n    local content = examples.standard_url\n    create_buffer(content, { 1, 0 })\n    local res = jump_backwards()\n    assert.is_nil(res)\n\n    for col = 1, #content do\n      set_cursor({ 1, col })\n      res = jump_backwards()\n      vim.deep_equal({ 1, 0 }, res)\n    end\n  end)\n\n  it(\"invalid jump before URL and start of URL\", function()\n    local content = examples.single_line_middle\n    local url_start = 4\n    create_buffer(content)\n    for col = 0, url_start do\n      set_cursor({ 1, col })\n      local res = jump_backwards()\n      assert.is_nil(res)\n    end\n  end)\n\n  it(\"simple jump to start of URL\", function()\n    local content = examples.single_line_middle\n    local url_start = 4\n    create_buffer(content)\n    for col = url_start + 1, #content do\n      set_cursor({ 1, col })\n      local res = jump_backwards()\n      vim.deep_equal({ 1, url_start }, res)\n    end\n  end)\n\n  it(\"multiline jump from anywhere in line\", function()\n    local content = examples.multi_line_just_links\n    local url_length = #examples.standard_url\n    create_buffer(content)\n\n    -- invalid jump\n    set_cursor({ 1, 0 })\n    local res = jump_backwards()\n    assert.is_nil(res)\n\n    -- jump to start of previous URL\n    local line_last = 4\n    for line = 1, line_last do\n      local expected = { line, 0 }\n      for col = 1, url_length do\n        set_cursor({ line, col })\n        res = jump_backwards()\n        vim.deep_equal(expected, res)\n      end\n      if line ~= line_last then\n        set_cursor({ line + 1, 0 })\n        vim.deep_equal(expected, res)\n      end\n    end\n  end)\n\n  it(\"multiline sandwich\", function()\n    local content = examples.multi_line_sandwich\n    local url_length = #examples.standard_url\n    create_buffer(content)\n\n    -- invalid jumps\n    for line = 1, 2 do\n      set_cursor({ line, 0 })\n      local res = jump_backwards()\n      assert.is_nil(res)\n    end\n\n    -- jumps to correct position\n    local expected = { 2, 0 }\n    for col = 1, url_length do\n      set_cursor({ 2, col })\n      local res = jump_backwards()\n      vim.deep_equal(expected, res)\n    end\n\n    -- line 3 jumps to line 2\n    set_cursor({ 3, 0 })\n    local res = jump_backwards()\n    vim.deep_equal(expected, res)\n  end)\n\n  it(\"jump chain\", function()\n    local content = examples.multi_line_just_links\n    local url_length = #examples.standard_url\n    create_buffer(content, { 4, url_length })\n\n    for line = 4, 1, -1 do\n      local res = jump_backwards()\n      vim.deep_equal({ line, 0 }, res)\n    end\n\n    local res = jump_backwards()\n    assert.is_nil(res)\n  end)\nend)\n\ndescribe(\"forwards jump\", function()\n  after_each(teardown_windows)\n\n  it(\"empty buffer\", function()\n    local content = examples.empty\n    create_buffer(content, { 1, 0 })\n    local res = jump_forwards()\n    assert.is_nil(res)\n  end)\n\n  it(\"no URL\", function()\n    local content = examples.invalid\n    create_buffer(content)\n    for col = 0, #content do\n      set_cursor({ 1, col })\n      local res = jump_forwards()\n      assert.is_nil(res)\n    end\n  end)\n\n  it(\"just URL\", function()\n    local content = examples.standard_url\n    create_buffer(content)\n    for col = 0, #content do\n      set_cursor({ 1, col })\n      local res = jump_forwards()\n      assert.is_nil(res)\n    end\n  end)\n\n  it(\"same line single\", function()\n    local content = examples.single_line_middle\n    local url_start = 4\n    create_buffer(content)\n    for col = 0, url_start - 1 do\n      set_cursor({ 1, col })\n      local res = jump_forwards()\n      vim.deep_equal({ 1, url_start }, res)\n    end\n  end)\n\n  it(\"on last URL + no more\", function()\n    local content = examples.single_line_middle\n    local url_start = 4\n    create_buffer(content)\n    for col = url_start, #content do\n      set_cursor({ 1, col })\n      local res = jump_forwards()\n      assert.is_nil(res)\n    end\n  end)\n\n  it(\"multiline jump from anywhere in line\", function()\n    local content = examples.multi_line_just_links\n    local url_length = #examples.standard_url\n    create_buffer(content)\n    -- jumps to next line\n    for line = 1, 3 do\n      for col = 0, url_length do\n        set_cursor({ line, col })\n        local res = jump_forwards()\n        vim.deep_equal({ line + 1, 0 }, res)\n      end\n    end\n    -- last line\n    for col = 0, url_length do\n      set_cursor({ 4, col })\n      local res = jump_forwards()\n      assert.is_nil(res)\n    end\n  end)\n\n  it(\"multiline sandwich\", function()\n    local content = examples.multi_line_sandwich\n    local url_length = #examples.standard_url\n    create_buffer(content, { 1, 0 })\n\n    -- line 1 jumps to line 2\n    local res = jump_forwards()\n    vim.deep_equal({ 2, 0 }, res)\n\n    -- line 2 onwards invalid\n    for col = 0, url_length do\n      set_cursor({ 2, col })\n      res = jump_forwards()\n      assert.is_nil(res)\n    end\n    set_cursor({ 3, 0 })\n    res = jump_forwards()\n    assert.is_nil(res)\n  end)\n\n  it(\"jump chain\", function()\n    local content = examples.multi_line_just_links\n    create_buffer(content, { 1, 0 })\n\n    for line = 1, 3 do\n      local res = jump_forwards()\n      vim.deep_equal({ line + 1, 0 }, res)\n    end\n\n    local res = jump_forwards()\n    assert.is_nil(res)\n  end)\nend)\n"
  },
  {
    "path": "tests/urlview/capture_mock_spec.lua",
    "content": "local urlview = require(\"urlview\")\nlocal reset_config = require(\"urlview.config.helpers\").reset_defaults\nlocal search = require(\"urlview.search\")\nlocal search_helpers = require(\"urlview.search.helpers\")\n\ndescribe(\"mock vim.ui.select\", function()\n  local default_prefix = \"test-\"\n  before_each(function()\n    urlview.setup({ default_prefix = default_prefix })\n    search.test = search_helpers.generate_custom_search({\n      capture = \"%w+\",\n      format = \"%s\",\n    })\n    assert.is_not.Nil(search.test)\n  end)\n\n  after_each(function()\n    search.test = nil\n    reset_config()\n  end)\n\n  local original_ui_select = vim.ui.select\n\n  it(\"unique, sorted\", function()\n    local content = \"pears watermelon banana apple apple peach apricot watermelon\"\n    local expected = vim.tbl_map(function(v)\n      return default_prefix .. v\n    end, { \"apple\", \"apricot\", \"banana\", \"peach\", \"pears\", \"watermelon\" })\n\n    vim.ui.select = function(items)\n      assert.same(expected, items)\n    end\n\n    urlview.search(\"test\", { content = content, unique = true, sorted = true })\n    vim.ui.select = original_ui_select\n  end)\nend)\n\ndescribe(\"mock telescope\", function() end)\n"
  },
  {
    "path": "tests/urlview/capture_multiple_spec.lua",
    "content": "local urlview = require(\"urlview\")\nlocal extract_links_from_content = require(\"urlview.search.helpers\").content\nlocal reset_config = require(\"urlview.config.helpers\").reset_defaults\nlocal assert_tbl_same_any_order = require(\"tests.urlview.helpers\").assert_tbl_same_any_order\n\ndescribe(\"multiple captures\", function()\n  it(\"separate lines\", function()\n    local content = [[\n      http://google.com\n      https://www.google.com\n    ]]\n    local result = extract_links_from_content(content)\n    assert_tbl_same_any_order({ \"http://google.com\", \"https://www.google.com\" }, result)\n  end)\n\n  it(\"same line\", function()\n    local content = \"http://google.com https://www.github.com\"\n    local result = extract_links_from_content(content)\n    assert_tbl_same_any_order({ \"http://google.com\", \"https://www.github.com\" }, result)\n  end)\nend)\n\ndescribe(\"unique captures\", function()\n  it(\"same link\", function()\n    local content = [[\n      http://google.com\n      http://google.com\n    ]]\n    local result = extract_links_from_content(content)\n    assert_tbl_same_any_order({ \"http://google.com\" }, result)\n  end)\n\n  it(\"different prefix / uri protocol, same default prefix\", function()\n    urlview.setup({\n      default_prefix = \"https://\",\n    })\n\n    local content = [[\n      https://www.google.com\n      www.google.com\n    ]]\n    local result = extract_links_from_content(content)\n    assert_tbl_same_any_order({ \"https://www.google.com\" }, result)\n\n    reset_config()\n  end)\n\n  it(\"different prefix / uri protocol, prefer specified\", function()\n    urlview.setup({\n      default_prefix = \"http://\",\n    })\n\n    local content = [[\n      https://www.google.com\n      www.google.com\n    ]]\n    local result = extract_links_from_content(content)\n    assert_tbl_same_any_order({ \"https://www.google.com\" }, result)\n\n    reset_config()\n  end)\n\n  it(\"different paths\", function()\n    local content = [[\n      https://www.google.com/search?q=vim\n      https://www.google.com/search?q=nvim\n    ]]\n    local result = extract_links_from_content(content)\n    assert_tbl_same_any_order({ \"https://www.google.com/search?q=vim\", \"https://www.google.com/search?q=nvim\" }, result)\n  end)\nend)\n"
  },
  {
    "path": "tests/urlview/capture_process_spec.lua",
    "content": "local urlview = require(\"urlview\")\nlocal search = require(\"urlview.search\")\nlocal search_helpers = require(\"urlview.search.helpers\")\nlocal reset_config = require(\"urlview.config.helpers\").reset_defaults\nlocal assert_tbl_same_any_order = require(\"tests.urlview.helpers\").assert_tbl_same_any_order\nlocal prepare_links = require(\"urlview.utils\").process_links\nlocal extract_links_from_content = search_helpers.content\n\ndescribe(\"HTTP(s) protocol fill in\", function()\n  local default_prefix = \"https://\"\n  before_each(function()\n    urlview.setup({ default_prefix = default_prefix })\n  end)\n\n  after_each(function()\n    reset_config()\n  end)\n\n  it(\"link with http protocol\", function()\n    local url = \"http://example.com\"\n    local links = extract_links_from_content(url)\n    local prepared_links = prepare_links(links)\n    assert.same({ url }, prepared_links)\n  end)\n\n  it(\"link with https protocol\", function()\n    local url = \"https://example.com\"\n    local links = extract_links_from_content(url)\n    local prepared_links = prepare_links(links)\n    assert.same({ url }, prepared_links)\n  end)\n\n  it(\"link missing either protocol\", function()\n    local url = \"www.example.com\"\n    local links = extract_links_from_content(url)\n    local prepared_links = prepare_links(links)\n    assert.same({ default_prefix .. url }, prepared_links)\n  end)\nend)\n\ndescribe(\"unique links\", function()\n  before_each(function()\n    urlview.setup({\n      default_prefix = \"\",\n    })\n    search.test = search_helpers.generate_custom_search({\n      capture = \"%d\",\n      format = \"%s\",\n    })\n    assert.is_not.Nil(search.test)\n  end)\n\n  after_each(function()\n    search.test = nil\n    reset_config()\n  end)\n\n  local content = \"1 1 2 2 3 3 4 4 5 5\"\n\n  it(\"keep duplicates\", function()\n    local links = search.test({ content = content })\n    local prepared_links = prepare_links(links, { unique = false })\n    assert_tbl_same_any_order({ \"1\", \"1\", \"2\", \"2\", \"3\", \"3\", \"4\", \"4\", \"5\", \"5\" }, prepared_links)\n  end)\n\n  it(\"filter duplicates\", function()\n    local links = search.test({ content = content })\n    local prepared_links = prepare_links(links, { unique = true })\n    assert.same({ \"1\", \"2\", \"3\", \"4\", \"5\" }, prepared_links)\n  end)\nend)\n\ndescribe(\"sorted links\", function()\n  local default_prefix = \"https://\"\n  before_each(function()\n    urlview.setup({ default_prefix = default_prefix, sort = true })\n  end)\n\n  after_each(function()\n    reset_config()\n  end)\n\n  it(\"URLs missing protocol fixed and sorted alphabetically\", function()\n    local content = [[\n    www.google.com\n    https://google.com\n    https://github.com/axieax/urlview.nvim\n    www.example.com\n    http://github.com/helloM\n    http://github.com/helloP\n    http://github.com/hellon\n    ]]\n\n    local links = extract_links_from_content(content)\n    local prepared_links = prepare_links(links)\n    local expected = {\n      \"http://github.com/helloM\",\n      \"http://github.com/hellon\",\n      \"http://github.com/helloP\",\n      \"https://github.com/axieax/urlview.nvim\",\n      \"https://google.com\",\n      default_prefix .. \"www.example.com\",\n      default_prefix .. \"www.google.com\",\n    }\n\n    assert.same(expected, prepared_links)\n  end)\nend)\n"
  },
  {
    "path": "tests/urlview/capture_single_spec.lua",
    "content": "local urlview = require(\"urlview\")\nlocal reset_config = require(\"urlview.config.helpers\").reset_defaults\nlocal assert_no_match = require(\"tests.urlview.helpers\").assert_no_match\nlocal assert_single_match = require(\"tests.urlview.helpers\").assert_single_match\n\ndescribe(\"no capture\", function()\n  it(\"empty string\", function()\n    assert_no_match(\"\")\n  end)\n\n  it(\"random\", function()\n    assert_no_match(\"asdfwiueyfksdlckvj\")\n  end)\n\n  it(\"com only\", function()\n    assert_no_match(\"test.com\")\n  end)\n\n  it(\"com path\", function()\n    assert_no_match(\"test.com/idk\")\n  end)\nend)\n\ndescribe(\"url-only simple capture\", function()\n  local default_prefix = \"https://\"\n  before_each(function()\n    urlview.setup({\n      default_prefix = default_prefix,\n    })\n  end)\n\n  after_each(function()\n    reset_config()\n  end)\n\n  it(\"http capture\", function()\n    local url = \"http://google.com\"\n    assert_single_match(url, url)\n  end)\n\n  it(\"https capture\", function()\n    local url = \"https://google.com\"\n    assert_single_match(url, url)\n  end)\n\n  it(\"www capture\", function()\n    local url = \"www.google.com\"\n    assert_single_match(url, url)\n  end)\n\n  it(\"http www capture\", function()\n    local url = \"http://www.google.com\"\n    assert_single_match(url, url)\n  end)\n\n  it(\"https www capture\", function()\n    local url = \"https://www.google.com\"\n    assert_single_match(url, url)\n  end)\n\n  it(\"trailing slash\", function()\n    local url = \"www.google.com/\"\n    assert_single_match(url, url)\n  end)\nend)\n\ndescribe(\"url-only path capture\", function()\n  local default_prefix = \"http://\"\n  before_each(function()\n    urlview.setup({\n      default_prefix = default_prefix,\n    })\n  end)\n\n  after_each(function()\n    reset_config()\n  end)\n\n  it(\"lol php capture\", function()\n    local url = \"https://who.even.uses/index.php\"\n    assert_single_match(url, url)\n  end)\n\n  it(\"https path capture\", function()\n    local url = \"https://google.com/path/to/idk\"\n    assert_single_match(url, url)\n  end)\n\n  it(\"www path capture\", function()\n    local url = \"www.google.com/path/to/idk\"\n    assert_single_match(url, url)\n  end)\n\n  it(\"url-encoded path query capture\", function()\n    local url = \"www.google.com/P%40%2Bh%20t35T%2F/1dk%3F?q=%3Da%25%3B\"\n    assert_single_match(url, url)\n  end)\n\n  it(\"query capture\", function()\n    local url = \"https://example.com/path/to/idk?q=axie&ax\"\n    assert_single_match(url, url)\n  end)\nend)\n"
  },
  {
    "path": "tests/urlview/helpers.lua",
    "content": "local M = {}\n\nlocal extract_links_from_content = require(\"urlview.search.helpers\").content\n\nfunction M.assert_no_match(content)\n  local result = extract_links_from_content(content)\n  assert.equals(0, #result)\nend\n\nfunction M.assert_single_match(url, expected_url)\n  local result = extract_links_from_content(url)\n  assert.same({ expected_url }, result)\nend\n\nfunction M.assert_tbl_same_any_order(expected, actual)\n  assert.same(#expected, #actual)\n  for _, e in ipairs(expected) do\n    assert.truthy(vim.tbl_contains(actual, e))\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "tests/urlview/jump_helpers.lua",
    "content": "local M = {}\n\nlocal jump = require(\"urlview.jump\")\nlocal utils = require(\"urlview.utils\")\n\nlocal active_windows = {}\n\nfunction M.set_cursor(pos)\n  vim.api.nvim_win_get_cursor = function()\n    return pos\n  end\nend\n\nfunction M.create_buffer(content, cursor_pos)\n  local lines = vim.split(content, \"\\n\")\n  local bufnr = vim.api.nvim_create_buf(false, true)\n  vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)\n  local winnr = vim.api.nvim_open_win(bufnr, false, {\n    relative = \"editor\",\n    row = 0,\n    col = 0,\n    width = 1,\n    height = 1,\n    zindex = 1,\n  })\n  active_windows[winnr] = true\n\n  vim.fn.getline = function(line_no)\n    return lines[line_no]\n  end\n  vim.api.nvim_buf_line_count = function()\n    return #lines\n  end\n\n  utils.fallback(cursor_pos, { 1, 0 })\n  M.set_cursor(cursor_pos)\nend\n\nfunction M.teardown_windows()\n  local windows = vim.tbl_keys(active_windows)\n  for _, winnr in ipairs(windows) do\n    if vim.api.nvim_win_is_valid(winnr) then\n      vim.api.nvim_win_close(winnr, true)\n      active_windows[winnr] = nil\n    end\n  end\nend\n\nfunction M.jump_forwards()\n  local res = jump.find_url(0, false)\n  if res then\n    M.set_cursor(res)\n  end\n  return res\nend\n\nfunction M.jump_backwards()\n  local res = jump.find_url(0, true)\n  if res then\n    M.set_cursor(res)\n  end\n  return res\nend\n\nreturn M\n"
  },
  {
    "path": "vim.toml",
    "content": "# SOURCE: https://github.com/jose-elias-alvarez/null-ls.nvim/blob/main/vim.toml\n[selene]\nbase = \"lua51\"\nname = \"vim\"\n\n[vim]\nany = true\n\n[[describe.args]]\ntype = \"string\"\n\n[[describe.args]]\ntype = \"function\"\n\n[[it.args]]\ntype = \"string\"\n\n[[it.args]]\ntype = \"function\"\n\n[[before_each.args]]\ntype = \"function\"\n\n[[after_each.args]]\ntype = \"function\"\n\n[assert.is_not]\nany = true\n\n[assert.matches]\nany = true\n\n[assert.has_error]\nany = true\n\n[[assert.equals.args]]\ntype = \"any\"\n\n[[assert.equals.args]]\ntype = \"any\"\n\n[[assert.equals.args]]\ntype = \"any\"\nrequired = false\n\n[[assert.same.args]]\ntype = \"any\"\n\n[[assert.same.args]]\ntype = \"any\"\n\n[[assert.truthy.args]]\ntype = \"any\"\n\n[[assert.falsy.args]]\ntype = \"any\"\n\n[[assert.spy.args]]\ntype = \"any\"\n\n[[assert.stub.args]]\ntype = \"any\"\n\n[[assert.is_nil.args]]\ntype = \"any\"\n"
  }
]