Repository: axieax/urlview.nvim Branch: main Commit: 813736e6891b Files: 31 Total size: 76.4 KB Directory structure: gitextract_1w34gv51/ ├── .github/ │ └── workflows/ │ └── default.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── doc/ │ └── urlview.txt ├── lua/ │ └── urlview/ │ ├── actions.lua │ ├── command.lua │ ├── config/ │ │ ├── constants.lua │ │ ├── default.lua │ │ ├── helpers.lua │ │ └── init.lua │ ├── init.lua │ ├── jump.lua │ ├── pickers.lua │ ├── search/ │ │ ├── helpers.lua │ │ ├── init.lua │ │ └── validation.lua │ └── utils.lua ├── selene.toml ├── stylua.toml ├── tests/ │ ├── init.vim │ └── urlview/ │ ├── capture_custom_searches_spec.lua │ ├── capture_jump_spec.lua │ ├── capture_mock_spec.lua │ ├── capture_multiple_spec.lua │ ├── capture_process_spec.lua │ ├── capture_single_spec.lua │ ├── helpers.lua │ └── jump_helpers.lua └── vim.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/default.yml ================================================ name: default on: pull_request: push: branches: [main] jobs: stylua: name: Check code style runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: JohnnyMorganz/stylua-action@v1 with: token: ${{ secrets.GITHUB_TOKEN }} args: --color always --check . selene: name: Lint code runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: NTBBloodbath/selene-action@v1.0.0 with: token: ${{ secrets.GITHUB_TOKEN }} args: --color always . test: name: Neovim test runner runs-on: ubuntu-latest strategy: matrix: neovim-version: [v0.9.0, v0.8.0, v0.7.0, stable, nightly] steps: - uses: actions/checkout@v2 with: path: urlview.nvim - uses: actions/checkout@v2 with: repository: nvim-lua/plenary.nvim path: plenary.nvim - uses: rhysd/action-setup-vim@v1 with: neovim: true version: ${{ matrix.neovim-version }} - run: make test working-directory: urlview.nvim timeout-minutes: 1 ================================================ FILE: .gitignore ================================================ doc/tags ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Andrew Xie Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ test: nvim --headless --noplugin -u tests/init.vim -c "PlenaryBustedDirectory tests/urlview {minimal_init = 'tests/init.vim'}" ================================================ FILE: README.md ================================================

🔎 urlview.nvim

Find and display URLs from a variety of search contexts

Neovim Version Repo Stars Repo Size

✨ UrlView is an extensible plugin for the [Neovim](https://neovim.io) text editor which essentially: 1. Finds URLs from a variety of **search contexts** (e.g. from a buffer, file, plugin URLs) 2. 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). 3. Performs **actions** on selected URLs, such as navigating to the URL in your preferred browser, or copying the link to your clipboard 🎯 Additional features and example use cases include: - Easily visualise all the URLs in a buffer or file (e.g. links in your Markdown documents) - Quickly accessing repo webpages for installed Neovim plugins (life-saver for config updates or browsing plugin documentation) - Ability to register custom searchers (e.g. Jira ticket numbers), pickers and actions (please see [docs](doc/urlview.txt) or `:h urlview.search-custom`) - Jumping to the previous or next URL in the active buffer (and opening the URL in your browser) > 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)). ## 📸 Screenshots ### 📋 Buffer Links `:UrlView` or `:UrlView buffer` ![buffer-demo](https://user-images.githubusercontent.com/62098008/161417569-e8103fc4-a009-4c4f-95a7-ea7e22cbb3df.png) ### 🔌 Plugin Links `:UrlView lazy`, `:UrlView packer`, or `:UrlView vimplug` depending on your plugin manager of choice ![packer-demo](https://user-images.githubusercontent.com/62098008/161417652-fd514310-a926-4ec7-af28-b2cfa3aa4b19.png) ## ⚡ Requirements - This plugin supports **Neovim v0.7** or later. - 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. - 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. ## 🚀 Usage ### Searching contexts 1. Use the command `:UrlView` to see all the URLs in the current buffer. - For your convenience, feel free to setup a keybind for this using `vim.keymap.set`: ```lua vim.keymap.set("n", "\\u", "UrlView", { desc = "View buffer URLs" }) vim.keymap.set("n", "\\U", "UrlView packer", { desc = "View Packer plugin URLs" }) ``` - You can also hit `:UrlView ` to see additional contexts that you can search from - e.g. `:UrlView packer` to view links for installed [packer.nvim](https://github.com/wbthomason/packer.nvim) plugins 2. You can optionally select a link to bring it up in your browser. ### Buffer URL navigation 1. You can use `[u` and `]u` (default bindings) to jump to the previous and next URL in the buffer respectively. 2. If desired, you can now press `gx` to open the URL under the cursor in your browser, with netrw. 3. This keymap can be altered under the `jump` config option. ## 📦 Installation Install this plugin with your package manager of choice. You can lazy load this plugin by the `UrlView` command if desired. - [packer.nvim](https://github.com/wbthomason/packer.nvim) ```lua use("axieax/urlview.nvim") ``` - [lazy.nvim](https://github.com/folke/lazy.nvim) ```lua "axieax/urlview.nvim" ``` ## ⚙️ Configuration This plugin supports plug-n-play, meaning you can get it up and running without any additional setup. However, you can customise the [default options](lua/urlview/config/default.lua) using the `setup` function: ```lua require("urlview").setup({ -- custom configuration options -- }) ``` Please check out the [documentation](doc/urlview.txt) for configuration options and details. ## 🎨 Pickers ### ✔️ Native (vim.ui.select) You 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. ### 🔭 Telescope - Optional picker option - Additional requirements (only if you're using this picker): [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) - You can use Telescope as your `default_picker` using the `require("urlview").setup` function - Alternatively, you can specify a picker dynamically with `:UrlView picker=telescope` ### 📑 fzf-lua - Optional picker option - Additional requirements (only if you're using this picker): [fzf-lua](https://github.com/ibhagwan/fzf-lua) - You can use fzf-lua as your `default_picker` using the `require("urlview").setup` function - Alternatively, you can specify a picker dynamically with `:UrlView picker=fzf_lua` ## 🚧 Stay Updated More 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 :) It 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. ================================================ FILE: doc/urlview.txt ================================================ *urlview.txt* Find and display URLs from a variety of search contexts ================================================================================ Table of Contents *urlview.contents* INTRODUCTION .......................................... |urlview| CONFIGURATION ......................................... |urlview.config| USAGE ................................................. |urlview.usage| SEARCH ................................................ |urlview.search| PICKERS ............................................... |urlview.pickers| ACTIONS ............................................... |urlview.actions| ================================================================================ INTRODUCTION *urlview* ✨ urlview.nvim is essentially a plugin which: 1. Finds URLs from a variety of |urlview.search| contexts 2. Displays these URLs in one of the |urlview.pickers| 3. Performs |urlview.actions| on selected URLs from the pickers ================================================================================ CONFIGURATION *urlview.config* urlview.nvim supports plug-n-play, meaning the default config is automatically set up. However, the default options can be configured using the `require("urlview").setup` function, with the configuration options below. *urlview.config-default_title* {default_title} string (default: "Links:") Forms part of the prompt title for the picker (` `). For example, for the buffer search context with a default_title of "Links:", the picker title becomes "Buffer Links:". The capitalisation of the first letter in the default_title determines the capitalisation of the search context in the prompt title as well. For example, a default_title of "links" will result in a prompt title of "buffer links". *urlview.config-default_picker* {default_picker} string (default: "native") Default picker for `:UrlView` commands. This should be any one of the pickers in |urlview.pickers|. *urlview.config-default_prefix* {default_prefix} string (default: "https://") Default prefix for URLs missing a HTTP protocol (e.g. "www.google.com" becomes "https://www.google.com"). Such a protocol is required for |urlview.actions| to be able to navigate to URLs. Another suggested option is "http://", although it is less secure than the default HTTPS protocol. *urlview.config-default_action* {default_action} string (default: "netrw") Default action to take upon selecting a URL from a picker. This should be any one of the actions in |urlview.actions|. *urlview.config-default_register* {default_register} string (default: "+") Default register to use when yankying. *urlview.config-default_include_branch* {default_include_branch} boolean (default: false) Default option for whether plugin URLs should link to the branch used by your package manager, for |urlview.search-lazy|, |urlview.search-packer| or |urlview.search-vimplug|. When this option is enabled, navigated links will open the plugin's repository to the specific branch specified to your plugin manager. *urlview.config-unique* {unique} boolean (default: true) Enable to ensure links shown in the picker are unique (i.e. no duplicates). *urlview.config-sorted* {sorted} boolean (default: true) Enable to ensure links shown in the picker are sorted alphabetically. *urlview.config-log_level_min* {log_level_min} `vim.log.levels` enum or int (default: `vim.log.levels.INFO`) Minimum log level for output from this plugin. Lower logging levels will be ignored. >lua vim.log.levels = { TRACE = 0, DEBUG = 1, INFO = 2, WARN = 3 ERROR = 4, OFF = 5, -- Neovim v0.8+ } < The default value of `vim.log.levels.INFO` means that INFO, WARN and ERROR logs will all be displayed. Similarly, a log_level_min value of `vim.log.levels.WARN` means that only WARN and ERROR logs will be displayed. It is recommended for this value to be at least `vim.log.levels.WARN` to ensure warnings are appropriately logged. Setting this value to `vim.log.levels.OFF` (requires Neovim 0.8+) or `5` will effectively suppress all logs. *urlview.config-jump* {jump} table (map) Registers keymaps for jumps to the previous or next URL in the buffer. Fields: {prev} string (default: "[u") Mapping to jump to the previous URL in the buffer. Set to "" to disable. {next} string (default: "]u") Mapping to jump to the previous URL in the buffer. Set to "" to disable. ================================================================================ USAGE *urlview.usage* For normal usage, interaction with this plugin is mostly achieved through the `:UrlView` command. This command provides completion to assist with specifying appropriate search contexts and respective options. Calling `:UrlView` without any additional arguments is the same as calling `:UrlView buffer`, which finds URLs in the current buffer with the default options configured ( |default_title|, |default_picker|, |default_action|, |unique|, |sorted|, etc. ). The command expects arguments in the format `:UrlView `, e.g. >lua :UrlView buffer bufnr=1 :UrlView file filepath=/etc/hosts picker=telescope :UrlView packer sorted=false < ================================================================================ SEARCH *urlview.search* `urlview.search` provides functions which finds and extracts URLs from a particular search context. The default search contexts can be found by the exposed functions in the `urlview.search` module (or `urlview/search/init.lua`). Buffer Search Context *urlview.search-buffer* The buffer search context finds URLs in a specific buffer (current by default). To search a particular buffer, provide the desired buffer number with `:UrlView buffer bufnr=`. File Search Context *urlview.search-file* The file search context allows a user to find URLs in a particular file. This requires passing in the parameter `filepath` for URLs in the desired file to be searched, for example with `:UrlView file filepath="/etc/hosts"`. Lazy Search Context *urlview.search-lazy* This search context resolves Git repository URLs for plugins installed with the lazy.nvim plugin manager. Invoked with `:UrlView lazy`. Packer Search Context *urlview.search-packer* This search context resolves Git repository URLs for plugins installed with the packer.nvim plugin manager. Invoked with `:UrlView packer`. vim-plug Search Context *urlview.search-vimplug* This search context resolves Git repository URLs for plugins installed with the vim-plug plugin manager. Invoked with `:UrlView vimplug`. Registering a custom search context *urlview.search-custom* Custom search contexts can be registered for searching with `:UrlView `. The `generate_custom_search` function from the `urlview.search.helpers` module can be used to quickly generate a function for capturing a Lua pattern and optionally formatting it with `string.format`. The following resource is very helpful for understanding basic Lua patterns: - https://riptutorial.com/lua/example/20315/lua-pattern-matching This function can then be assigned to the `urlview.search` module for use as a search context to be selected with the `:UrlView` command (with completion). Here is an example which finds Jira ticket numbers in the format of "AXIE-" followed by any number (capture field). This entire pattern gets captured and can then be embedded into the format string in the format field, allowing a user to directly navigate to the Jira ticket in their browser. >lua local search = require("urlview.search") local search_helpers = require("urlview.search.helpers") search["jira"] = search_helpers.generate_custom_search({ capture = "AXIE%-%d+", format = "https://jira.axieax.com/browse/%s", }) This allows for captures such as "AXIE-1", "AXIE-17", "AXIE-132". Feel free to adjust the `capture` field to suit your needs. A custom function can also be registered for a search context, as well as additional parameters with something like >lua local search = require("urlview.search") search["fruits"] = function(opts) local fruits = { "apple", "banana", "watermelon" } if opts.include_tomato then table.insert(fruits, "tomato") end return fruits end Additional parameters can be passed into the custom search context using the `:UrlView` command, for example `:UrlView fruits include_tomato` or `:UrlView fruits include_tomato=true`. Other types also work. Please see `urlview/search/init.lua` for more examples. If you have a useful custom search context, feel free to share it in https://github.com/axieax/urlview.nvim/discussions/40 for others to use (and potentially make it a built-in search context)! ================================================================================ PICKERS *urlview.pickers* `urlview.pickers` are used to display the results from |urlview.search|. vim.ui.select *urlview.pickers-native* This picker uses the built-in function `vim.ui.select` to display results. It is recommended to use a UI-extension for `vim.ui.select` for enhanced functionality (similar to |urlview.pickers-telescope|), including customising popup locations and behaviour, and searching (even fuzzy-searching) through results. An example of such a plugin is dressing.nvim (https://github.com/stevearc/dressing.nvim). telescope *urlview.pickers-telescope* This picker uses the Telescope plugin (https://github.com/nvim-telescope/telescope.nvim) as a picker to display results. Please note that this requires installing Telescope as a plugin in order for it to be used as a picker. Registering a custom picker *urlview.pickers-custom* A recommendation for registering a custom picker is to first check out a `vim.ui.select` UI-extension such as dressing.nvim (https://github.com/stevearc/dressing.nvim) to see if your desired picker can be used for the native picker (e.g. nui, fzf). In the case that you want to write your own picker, you can register it by adding it as a function to the `urlview.pickers` module. >lua local pickers = require("urlview.pickers") pickers["fzf"] = function(items, opts) -- TODO end < Please check out `urlview/pickers.lua` for examples on how to do this. ================================================================================ ACTIONS *urlview.actions* `urlview.actions` determine the behaviour when a URL is selected with one of the |urlview.pickers|. netrw *urlview.actions-netrw* The `netrw` action uses the built-in netrw feature to open a given URL in your browser. However, some users may have this feature disabled, either explicitly or due to "netrw hijack" behaviour from file-explorer Neovim plugins. Due to this, if urlview detects that netrw is disabled, it will use |urlview.actions-system| as a fallback by default. system *urlview.actions-system* This action opens URL in your system's default browser depending on your operating system. If your system is not supported, please raise a GitHub issue to have it included as a built-in options. Otherwise, specify a custom action ( |urlview.actions-custom|) for your use case. clipboard *urlview.actions-clipboard* This action copies selected URLs to the system clipboard (specifically to Neovim's {+} register). Registering a custom action *urlview.actions-custom* There are two main types of custom actions: 1. Execute a shell command which takes in your URL as an argument This is the main use case for custom actions - specifying a browser such as `chromium` or `firefox` to open your URL with. By default, this executes the provided shell command and passes in the URL as an argument, such as >bash $ chromium 'https://www.google.com' < which launches Google in the chromium browser. This can be any executable (be sure to find the correct path to your desired application). 2. Lua function With urlview.nvim's principle of extensibility in mind, you can also register a custom action as a Lua function by adding it to the `urlview.actions` module, like so: >lua local actions = require("urlview.actions") actions["spectate"] = function(raw_url) -- TODO end < Please refer to `urlview/actions.lua` for examples, and feel free to create a GitHub issue or pull request to add your custom Lua function action as a built-in action if you think others can use it as well! vim:tw=78:ts=8:noet:ft=help:norl: ================================================ FILE: lua/urlview/actions.lua ================================================ local M = {} local utils = require("urlview.utils") local config = require("urlview.config") --- Use command to open the URL ---@param cmd string @name of executable to run ---@param args string|table @arg(s) to pass into cmd (unescaped URL string or table of args) local function shell_exec(cmd, args) if cmd and vim.fn.executable(cmd) == 1 then -- NOTE: `vim.fn.system` shellescapes arguments local cmd_args = { cmd } vim.list_extend(cmd_args, type(args) == "table" and args or { args }) local err = vim.fn.system(cmd_args) if vim.v.shell_error ~= 0 or err ~= "" then utils.log( string.format("Failed to navigate link with cmd `%s` and args `%s`\n%s", cmd, args, err), vim.log.levels.ERROR ) end else utils.log( string.format("Cannot use command `%s` to navigate links (either empty or non-executable)", cmd), vim.log.levels.ERROR ) end end --- Use `netrw` to navigate to a URL ---@param raw_url string @unescaped URL function M.netrw(raw_url) local url = vim.fn.shellescape(raw_url) local ok, err = pcall(vim.cmd, string.format("call netrw#BrowseX(%s, netrw#CheckIfRemote(%s))", url, url)) if not ok and vim.startswith(err, "Vim(call):E117: Unknown function") then -- lazily use system action if netrw is disabled M.system(raw_url) end end --- Use the user's default browser to navigate to a URL ---@param raw_url string @unescaped URL function M.system(raw_url) local os = utils.os if os == "Darwin" then -- MacOS shell_exec("open", raw_url) elseif os == "Linux" or os == "FreeBSD" then -- Linux and FreeBSD shell_exec("xdg-open", raw_url) elseif os:match("Windows") then -- Windows -- HACK: `start` cmd itself doesn't exist but lives under `cmd` shell_exec("cmd", { "/C", "start", raw_url }) else utils.log( "Unsupported operating system for `system` action. Please raise a GitHub issue for " .. os, vim.log.levels.WARN ) end end --- Copy URL to clipboard ---@param raw_url string @unescaped URL function M.clipboard(raw_url) vim.fn.setreg(config.default_register, raw_url) utils.log(string.format("URL %s copied to clipboard", raw_url), vim.log.levels.INFO) end return setmetatable(M, { -- execute action as command if it is not one of the above module keys __index = function(_, k) if k ~= nil then return function(raw_url) return shell_exec(k, raw_url) end end end, }) ================================================ FILE: lua/urlview/command.lua ================================================ local M = {} local utils = require("urlview.utils") local search_contexts = require("urlview.search") local search_validation = require("urlview.search.validation") --- Processes arguments provided through the `UrlView` command for `M.search` local function command_search(res) local opts = {} local context = res.fargs[1] local option_args = vim.list_slice(res.fargs, 2) -- process provided options for _, arg in ipairs(option_args) do local split = vim.split(arg, "=", { plain = true }) if #split == 1 then opts[arg] = true elseif #split == 2 then local key, value = unpack(split) -- remove beginning and trailing quotes from value if present local inner = value:match([[^['"](.*)['"]$]]) if inner ~= nil then value = inner end -- type conversion if vim.tbl_contains({ "true", "false" }, value:lower()) then value = utils.string_to_boolean(value) end opts[key] = value else utils.log("Unable to parse argument " .. arg, vim.log.levels.ERROR) return end end require("urlview").search(context, opts) end local additional_opts = { "title", "picker", "action", "sorted" } local function command_completion(_, line) local args = vim.split(line, "%s+") local nargs = #args - 2 if nargs == 0 then -- search context completion local contexts = vim.tbl_keys(search_contexts) utils.alphabetical_sort(contexts) return contexts else -- opts completion local context = args[2] local context_opts = search_validation[context]() local accepted_opts = vim.list_extend(context_opts, additional_opts) utils.alphabetical_sort(accepted_opts) return vim.tbl_map(function(value) return value .. "=" end, accepted_opts) end end function M.register_command() vim.api.nvim_create_user_command("UrlView", command_search, { desc = "Find URLs in the current buffer or another search context", complete = command_completion, nargs = "*", }) end return M ================================================ FILE: lua/urlview/config/constants.lua ================================================ local constants = { -- SEE: lua pattern matching (https://riptutorial.com/lua/example/20315/lua-pattern-matching) -- regex equivalent: [A-Za-z0-9@:%._+~#=/\-?&]* pattern = "[%w@:%%._+~#=/%-?&]*", http_pattern = "https?://", www_pattern = "www%.", } return constants ================================================ FILE: lua/urlview/config/default.lua ================================================ local default_config = { -- Prompt title (` `, e.g. `Buffer Links:`) default_title = "Links:", -- Default picker to display links with -- Options: "native" (vim.ui.select) or "telescope" default_picker = "native", -- Set the default protocol for us to prefix URLs with if they don't start with http/https default_prefix = "https://", -- Command or method to open links with -- Options: "netrw", "system" (default OS browser), "clipboard"; or "firefox", "chromium" etc. -- By default, this is "netrw", or "system" if netrw is disabled default_action = "netrw", -- Set the register to use when yanking -- Default: + (system clipboard) default_register = "+", -- Whether plugin URLs should link to the branch used by your package manager default_include_branch = false, -- Ensure links shown in the picker are unique (no duplicates) unique = true, -- Ensure links shown in the picker are sorted alphabetically sorted = true, -- Minimum log level (recommended at least `vim.log.levels.WARN` for error detection warnings) log_level_min = vim.log.levels.INFO, -- Keymaps for jumping to previous / next URL in buffer jump = { prev = "[u", next = "]u", }, } return default_config ================================================ FILE: lua/urlview/config/helpers.lua ================================================ local M = {} local config = require("urlview.config") local default_config = require("urlview.config.default") function M.reset_defaults() config._options = default_config end function M.update_config(user_config) config._options = vim.tbl_deep_extend("force", config._options, user_config) end return M ================================================ FILE: lua/urlview/config/init.lua ================================================ local M = { _options = {}, } return setmetatable(M, { -- general get and set operations refer to the internal table `_options` __index = function(_, k) return M._options[k] end, __newindex = function(_, k, v) M._options[k] = v end, }) ================================================ FILE: lua/urlview/init.lua ================================================ local M = {} local actions = require("urlview.actions") local command = require("urlview.command") local config = require("urlview.config") local config_helpers = require("urlview.config.helpers") local jump = require("urlview.jump") local search = require("urlview.search") local search_validation = require("urlview.search.validation") local pickers = require("urlview.pickers") local utils = require("urlview.utils") --- Searchs the provided context for links ---@param ctx string where to search (default: search buffer) ---@param opts table (map, optional) function M.search(ctx, opts) ctx = utils.fallback(ctx, "buffer") opts = utils.fallback(opts, {}) opts.action = utils.fallback(opts.action, config.default_action) local picker = utils.fallback(opts.picker, config.default_picker) if not opts.title then local should_capitalise = string.match(config.default_title, "^%u") local ctx_title = utils.ternary(should_capitalise, ctx:gsub("^%l", string.upper), ctx) opts.title = string.format("%s %s", ctx_title, config.default_title) end -- search ctx for links and display with picker opts = search_validation[ctx](opts) local links = search[ctx](opts) links = utils.process_links(links, opts) if links and not vim.tbl_isempty(links) then if type(opts.action) == "string" then opts.action = actions[opts.action] end pickers[picker](links, opts) else utils.log("No links found in context " .. ctx, vim.log.levels.INFO) end end local function autoload() config_helpers.reset_defaults() command.register_command() end autoload() --- Custom setup function --- Not required to be called unless user wants to modify the default config ---@param user_config table (optional) function M.setup(user_config) user_config = utils.fallback(user_config, {}) config_helpers.update_config(user_config) jump.register_mappings(config.jump) end return M ================================================ FILE: lua/urlview/jump.lua ================================================ local M = {} -- NOTE: line numbers are 1-indexed, column numbers are 0-indexed local utils = require("urlview.utils") local search_helpers = require("urlview.search.helpers") local END_COL = -1 --- Return the starting positions of `match` in `line` ---@param line string ---@param match string ---@param offset number @added to each position ---@return table (list) of offsetted starting indicies function M.line_match_positions(line, match, offset) local res = {} local init = 1 while init <= #line do local start, finish = line:find(match, init, true) if start == nil then return res end table.insert(res, start + offset - 1) init = finish end return res end --- Returns a starting column position not on a URL ---@param line_start number @line number at cursor ---@param col_start number @column number at cursor ---@param reversed boolean @direction ---@return number @corrected starting column function M.correct_start_col(line_start, col_start, reversed) local full_line = vim.fn.getline(line_start) local matches = search_helpers.content(full_line) for _, match in ipairs(matches) do local positions = M.line_match_positions(full_line, match, 0) for _, position in ipairs(positions) do local url_end = position + #match local on_url = col_start >= position and col_start < url_end -- edge case for going backwards with cursor at start of URL if on_url and reversed and position == col_start then return math.max(col_start - 1, 0) -- generally if on a URL, move column to be after the URL elseif on_url then return url_end end end end return col_start end local reversed_sort_function_lookup = { -- reversed == true: descending sort [true] = function(a, b) return a > b end, -- reversed == false: ascending sort [false] = function(a, b) return a < b end, } --- Finds the position of the previous / next URL ---@param winnr number @id of current window ---@param reversed boolean @direction false for forward, true for backwards ---@return table|nil @position function M.find_url(winnr, reversed) local line_no, col_no = unpack(vim.api.nvim_win_get_cursor(winnr)) local total_lines = vim.api.nvim_buf_line_count(0) col_no = M.correct_start_col(line_no, col_no, reversed) local sort_function = reversed_sort_function_lookup[reversed] local line_last = utils.ternary(reversed, 0, total_lines + 1) while line_no ~= line_last do local full_line = vim.fn.getline(line_no) col_no = utils.ternary(col_no == END_COL, #full_line, col_no) local line = utils.ternary(reversed, full_line:sub(1, col_no), full_line:sub(col_no + 1)) local matches = search_helpers.content(line) if not vim.tbl_isempty(matches) then -- sorted table(list) of starting column numbers for URLs in line -- normal order: ascending, reversed order: descending local indices = {} for _, match in ipairs(matches) do local offset = utils.ternary(reversed, 0, col_no) vim.list_extend(indices, M.line_match_positions(line, match, offset)) end table.sort(indices, sort_function) -- find first valid (before or after current column) for _, index in ipairs(indices) do local valid = utils.ternary(reversed, index <= col_no, index >= col_no) if valid then return { line_no, index } end end end line_no = utils.ternary(reversed, line_no - 1, line_no + 1) col_no = utils.ternary(reversed, END_COL, 0) end end --- Forward / backward jump generator ---@param reversed boolean @direction false for forward, true for backwards ---@return function @when called, jumps to the URL in the given direction local function goto_url(reversed) return function() local direction = utils.ternary(reversed, "previous", "next") local winnr = vim.api.nvim_get_current_win() local pos = M.find_url(winnr, reversed) if not pos then utils.log(string.format("Cannot find any %s URLs in buffer", direction), vim.log.levels.INFO) return end if vim.api.nvim_win_is_valid(winnr) then vim.cmd("normal! m'") -- add to jump list vim.api.nvim_win_set_cursor(winnr, pos) else utils.log( string.format("The %s URL was found in window number %s, which is no longer valid", direction, winnr), vim.log.levels.WARN ) end end end --- Jump to the next URL M.next_url = goto_url(false) --- Jump to the previous URL M.prev_url = goto_url(true) --- Register URL jump mappings ---@param jump_opts table function M.register_mappings(jump_opts) if type(jump_opts) ~= "table" then utils.log( "Invalid type for option `jump` (expected: table with `prev` and `next` key mappings)", vim.log.levels.WARN ) else if jump_opts.prev ~= "" then vim.keymap.set("n", jump_opts.prev, function() require("urlview.jump").prev_url() end, { desc = "Previous URL" }) end if jump_opts.next ~= "" then vim.keymap.set("n", jump_opts.next, function() require("urlview.jump").next_url() end, { desc = "Next URL" }) end end end return M ================================================ FILE: lua/urlview/pickers.lua ================================================ local M = {} local utils = require("urlview.utils") --- Displays items using the vim.ui.select picker ---@param items table (list) of strings ---@param opts table (map) of options function M.native(items, opts) local options = { prompt = opts.title } local function on_choice(item, _) if item then opts.action(item) end end vim.ui.select(items, options, on_choice) end --- Displays items using the fzf-lua picker ---@param items table (list) of strings ---@param opts table (map) of options function M.fzf_lua(items, opts) local fzf = pcall(require, "fzf-lua") if not fzf then utils.log("fzf-lua is not installed, defaulting to native vim.ui.select picker.", vim.log.levels.INFO) return M.native(items, opts) end require("fzf-lua").fzf_exec(items, { prompt = opts.title, fzf_opts = { ["--multi"] = true }, actions = { ["default"] = function(selected) for _, entry in ipairs(selected) do opts.action(entry) end end, }, }) end --- Displays items using the Telescope picker ---@param items table (list) of strings ---@param opts table (map) of options function M.telescope(items, opts) local telescope = pcall(require, "telescope") if not telescope then utils.log("Telescope is not installed, defaulting to native vim.ui.select picker.", vim.log.levels.INFO) return M.native(items, opts) end local actions = require("telescope.actions") local action_state = require("telescope.actions.state") local conf = require("telescope.config").values local finders = require("telescope.finders") local pickers = require("telescope.pickers") pickers .new(opts, { prompt_title = opts.title, finder = finders.new_table({ results = items, }), sorter = conf.generic_sorter(opts), attach_mappings = function(prompt_bufnr, _) actions.select_default:replace(function() local picker = action_state.get_current_picker(prompt_bufnr) local multi = picker:get_multi_selection() local single = picker:get_selection() actions.close(prompt_bufnr) if #multi > 0 then for _, entry in ipairs(multi) do opts.action(entry[1]) end elseif single[1] then opts.action(single[1]) end end) return true end, }) :find() end return setmetatable(M, { -- use default `vim.ui.select` when provided an invalid picker __index = function(_, k) if k ~= nil then utils.log(k .. " is not a valid picker, defaulting to native vim.ui.select picker.", vim.log.levels.INFO) return M.native end end, }) ================================================ FILE: lua/urlview/search/helpers.lua ================================================ local M = {} local constants = require("urlview.config.constants") local utils = require("urlview.utils") --- Extracts content from a given buffer ---@param bufnr number (optional) ---@return string @content of buffer function M.get_buffer_content(bufnr) bufnr = utils.fallback(bufnr, 0) if not vim.api.nvim_buf_is_valid(bufnr) then utils.log(string.format("Invalid buffer number provided: %s", bufnr), vim.log.levels.ERROR) return "" end return table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n") end --- Extracts content from a given file ---@param filepath string @path to file ---@return string @content of file (or empty string if file cannot be open) function M.read_file(filepath) local f, err = io.open(vim.fn.expand(filepath), "r") if f == nil then utils.log(err, vim.log.levels.ERROR) return "" end local content = f:read("*all") f:close() return content end --- Extracts urls from the given content ---@param content string ---@return table (list) of strings (extracted links) function M.content(content) ---@type table (map) of string (url base pattern) to string (prefix / uri protocol) local captures = {} -- NOTE: this method enforces unique matches regardless of config (before a general pattern is implemented) -- Extract URLs starting with http:// or https:// for capture in content:gmatch(constants.http_pattern .. "%w" .. constants.pattern) do local prefix = capture:match(constants.http_pattern) local url = capture:gsub(constants.http_pattern, "") captures[url] = prefix end -- Extract URLs starting with www, excluding already extracted http(s) URLs for capture in content:gmatch(constants.www_pattern .. "%w" .. constants.pattern) do if not captures[capture] then captures[capture] = "" end end -- Combine captures local links = {} for url, prefix in pairs(captures) do local link = prefix .. url if link ~= "" then table.insert(links, link) end end return links end --- Extract @captures from @content and display them as @formats ---@param content string @content to extract from ---@param capture string @capture pattern to extract ---@param format string @Optional format pattern to display ---@return table @list of extracted links function M.extract_pattern(content, capture, format) local captures = {} for c in content:gmatch(capture) do local fmt = format and string.format(format, c) or c table.insert(captures, fmt) end return captures end --- Extract Git links from @plugins_spec ---@param plugins_spec table @map of specs for plugins ---@param uri_key string @key in plugin_spec containing the Git URL ---@param include_branch boolean @whether to include the branch in the URL ---@return table @list of extracted links function M.extract_plugins_spec(plugins_spec, uri_key, include_branch) local function filter_files(plugin_url) if plugin_url == nil then return false end local fs_stat = vim.loop.fs_stat(plugin_url) return not fs_stat or vim.tbl_isempty(fs_stat) end local function extract_key(plugin_spec) if plugin_spec[uri_key] == nil then return nil end local uri = plugin_spec[uri_key]:gsub("%.git$", "") -- remove `.git` suffix if include_branch and plugin_spec.branch then uri = string.format("%s/tree/%s", uri, plugin_spec.branch) end return uri end local plugins = vim.tbl_map(extract_key, vim.tbl_values(plugins_spec or {})) return vim.tbl_filter(filter_files, plugins) end --- Generates a simple search function from a template table ---@param pattern table (map) with `capture` key and optional `format` key ---@return function|nil function M.generate_custom_search(pattern) if not pattern.capture then utils.log("Unable to generate custom search: please ensure that the table has 'capture' field", vim.log.levels.WARN) return nil end return function(opts) local content = opts.content or M.get_buffer_content(opts.bufnr) return M.extract_pattern(content, pattern.capture, pattern.format) end end return M ================================================ FILE: lua/urlview/search/init.lua ================================================ local M = {} local utils = require("urlview.utils") local search_helpers = require("urlview.search.helpers") -- NOTE: make sure to add accepted params for `opts` to `urlview.search.validation` as well if needed --- Extracts urls from the current buffer or a given buffer ---@param opts table (map) ---@return table (list) of strings (extracted links) function M.buffer(opts) local content = search_helpers.get_buffer_content(opts.bufnr) return search_helpers.content(content) end --- Extracts urls from a given file ---@param opts table (map) ---@return table (list) of strings (extracted links) function M.file(opts) local content = search_helpers.read_file(opts.filepath) return search_helpers.content(content) end --- Extracts urls of packer.nvim plugins ---@param opts table (map) ---@return table (list) of strings (extracted links) function M.packer(opts) -- selene: allow(global_usage) local plugins = _G.packer_plugins or {} return search_helpers.extract_plugins_spec(plugins, "url", opts.include_branch) end --- Extracts urls of lazy.nvim plugins ---@param opts table (map) ---@return table (list) of strings (extracted links) function M.lazy(opts) local ok, lazy = pcall(require, "lazy") local plugins = ok and lazy.plugins() or {} return search_helpers.extract_plugins_spec(plugins, "url", opts.include_branch) end --- Extracts urls of vim-plug plugins ---@param opts table (map) ---@return table (list) of strings (extracted links) function M.vimplug(opts) local plugins = vim.g.plugs or {} return search_helpers.extract_plugins_spec(plugins, "uri", opts.include_branch) end return setmetatable(M, { -- error check for invalid searcher (still allow function calls, but return empty table) __index = function(_, k) if k ~= nil then utils.log("Cannot search context " .. k, vim.log.levels.WARN) return function() return {} end end end, }) ================================================ FILE: lua/urlview/search/validation.lua ================================================ local M = {} local utils = require("urlview.utils") local config = require("urlview.config") --- Validates `opts` and sets default values if `opts` is provided, otherwise returns all possible accepted options ---@param opts table (map) of user options ---@param rules table (map) of vim.validate rules (with an optional fourth tuple member for default value) --- (option_key (string) -> { type (string), optional: boolean, default: any }) ---@return table (map) of updated user options if `opts` is provided, otherwise returns all possible accepted options as a table (list) local function verify_or_accept(opts, rules) if not opts then -- return valid options if `opts` not provided return vim.tbl_keys(rules) end local new_opts = vim.deepcopy(opts) for key, value in pairs(rules) do if #value == 4 and value[3] and opts[key] == nil then local default = value[4] new_opts[key] = default rules[key][1] = default end end vim.validate(rules) return new_opts end -- NOTE: validation for `urlview.search.init` functions go below --- Validation for the "buffer" search context ---@param opts table (map, optional) of user options ---@return table (map) of updated user options if `opts` is provided, otherwise returns all possible accepted options as a table (list) function M.buffer(opts) return verify_or_accept(opts, { bufnr = { opts.bufnr, "number", true, 0 }, }) end --- Validation for the "file" search context ---@param opts table (map, optional) of user options ---@return table (map) of updated user options if `opts` is provided, otherwise returns all possible accepted options as a table (list) function M.file(opts) return verify_or_accept(opts, { filepath = { opts.filepath, "string", false }, }) end --- Validation for the "packer" search context ---@param opts table (map, optional) of user options ---@return table (map) of updated user options if `opts` is provided, otherwise returns all possible accepted options as a table (list) function M.packer(opts) return verify_or_accept(opts, { include_branch = { opts.include_branch, "boolean", true, config.default_include_branch }, }) end --- Validation for the "lazy" search context ---@param opts table (map, optional) of user options ---@return table (map) of updated user options if `opts` is provided, otherwise returns all possible accepted options as a table (list) function M.lazy(opts) return verify_or_accept(opts, { include_branch = { opts.include_branch, "boolean", true, config.default_include_branch }, }) end --- Validation for the "vimplug" search context ---@param opts table (map, optional) of user options ---@return table (map) of updated user options if `opts` is provided, otherwise returns all possible accepted options as a table (list) function M.vimplug(opts) return verify_or_accept(opts, { include_branch = { opts.include_branch, "boolean", true, config.default_include_branch }, }) end return setmetatable(M, { __index = function(_, _) return function(opts) -- if `opts` provided, return `opts`, otherwise return all possible accepted options return utils.fallback(opts, {}) end end, }) ================================================ FILE: lua/urlview/utils.lua ================================================ local M = {} local config = require("urlview.config") local constants = require("urlview.config.constants") M.os = vim.loop.os_uname().sysname function M.alphabetical_sort(tbl) table.sort(tbl, function(a, b) return a:lower() < b:lower() end) end --- Processes links before being displayed ---@param links table @list of extracted links ---@param opts table @Optional options ---@return table @list of prepared links function M.process_links(links, opts) opts = M.fallback(opts, {}) local new_links = {} -- Attach missing HTTP(s) protocol for _, link in ipairs(links) do if not link:match("^" .. constants.http_pattern) then link = config.default_prefix .. link end table.insert(new_links, link) end -- Filter duplicate links -- NOTE: links with different protocols / www prefix / trailing slashes are not filtered to ensure links do not break if M.fallback(opts.unique, config.unique) then local map = {} for _, link in ipairs(new_links) do map[link] = true end new_links = vim.tbl_keys(map) end -- Sort links alphabetically (case insensitive) if M.fallback(opts.sorted, config.sorted) then M.alphabetical_sort(new_links) end return new_links end --- Determines whether to accept the current value or use a fallback value ---@param value any @value to check ---@param fallback_value any @fallback value to use ---@param fallback_comparison any @fallback comparison, defaults to nil ---@return any @value, or @fallback if @value is @fallback_comparison function M.fallback(value, fallback_value, fallback_comparison) return (value == fallback_comparison and fallback_value) or value end --- Mimics the ternary operator ---@param condition boolean @condition to check ---@param if_true any @value to return if @condition is true ---@param if_false any @value to return if @condition is false ---@return any @condition ? if_true : if_false function M.ternary(condition, if_true, if_false) return (condition and if_true) or if_false end --- Logs user warnings ---@param message string @message to log ---@param level integer|nil @log level, defaults to "warning" function M.log(message, level) level = M.fallback(level, vim.log.levels.WARN) if level >= config.log_level_min then vim.notify("[urlview.nvim] " .. message, level) end end --- Converts a boolean to a string ---@param value string @value to convert ---@return boolean|nil @value as a boolean, or nil if not a boolean function M.string_to_boolean(value) value = value:lower() local bool_map = { ["true"] = true, ["false"] = false } if not bool_map[value] then M.log("Could not convert " .. value .. " to boolean") end return bool_map[value] end return M ================================================ FILE: selene.toml ================================================ std = "vim" ================================================ FILE: stylua.toml ================================================ column_width = 120 line_endings = "Unix" indent_type = "Spaces" indent_width = 2 quote_style = "AutoPreferDouble" no_call_parentheses = false ================================================ FILE: tests/init.vim ================================================ set noswapfile set rtp+=../plenary.nvim set rtp+=../urlview.nvim runtime! plugin/plenary.vim ================================================ FILE: tests/urlview/capture_custom_searches_spec.lua ================================================ local search = require("urlview.search") local search_helpers = require("urlview.search.helpers") local assert_tbl_same_any_order = require("tests.urlview.helpers").assert_tbl_same_any_order describe("custom Jira searcher (template table)", function() before_each(function() search.jira = search_helpers.generate_custom_search({ capture = "AXIE%-%d+", format = "https://jira.axieax.com/browse/%s", }) assert.is_not.Nil(search.jira) end) after_each(function() search.jira = nil end) it("capture single", function() local content = "AXIE-1" local links = search.jira({ content = content }) assert_tbl_same_any_order({ "https://jira.axieax.com/browse/AXIE-1", }, links) end) it("capture multiple", function() local content = [[ AXIE-1 AXIE-12 AXIE-123 AXIE-1234 AXIE-12345 AXIE-123456 AXIE-1234567 AXIE-12345678 AXIE-123456789 AXIE-1234567890 ]] local links = search.jira({ content = content }) assert_tbl_same_any_order({ "https://jira.axieax.com/browse/AXIE-1", "https://jira.axieax.com/browse/AXIE-12", "https://jira.axieax.com/browse/AXIE-123", "https://jira.axieax.com/browse/AXIE-1234", "https://jira.axieax.com/browse/AXIE-12345", "https://jira.axieax.com/browse/AXIE-123456", "https://jira.axieax.com/browse/AXIE-1234567", "https://jira.axieax.com/browse/AXIE-12345678", "https://jira.axieax.com/browse/AXIE-123456789", "https://jira.axieax.com/browse/AXIE-1234567890", }, links) end) it("invalid captures ignored", function() local content = [[ AXIE AXIE1 AXIE--123 AXIE%-1 AXIE-! AXIE-AXIE AXIE-ax ]] local links = search.jira({ content = content }) assert_tbl_same_any_order({}, links) end) end) describe("overwrite default searcher", function() local builtin_search_buffer = search.buffer before_each(function() search.buffer = search_helpers.generate_custom_search({ capture = "l.ve", format = "i-%s-testing", }) end) after_each(function() search.buffer = builtin_search_buffer end) it("capture single", function() local content = "i love testing" local result = search.buffer({ content = content }) assert_tbl_same_any_order({ "i-love-testing", }, result) end) it("capture multiple", function() local content = "I live to see another day, I love Neovim" local result = search.buffer({ content = content }) assert_tbl_same_any_order({ "i-love-testing", "i-live-testing", }, result) end) end) describe("custom function", function() before_each(function() search.test = function(opts) return { opts.a or "default", opts.b or "default", opts.c or "default" } end end) after_each(function() search.test = nil end) it("capture one", function() local result = search.test({ a = "a" }) assert_tbl_same_any_order({ "a", "default", "default" }, result) end) it("capture two", function() local result = search.test({ a = "a", c = "c" }) assert_tbl_same_any_order({ "a", "c", "default" }, result) end) it("capture three", function() local result = search.test({ a = "a", b = "b", c = "c" }) assert_tbl_same_any_order({ "a", "b", "c" }, result) end) it("ignore extra", function() local result = search.test({ a = "a", b = "b", c = "c", d = "d" }) assert_tbl_same_any_order({ "a", "b", "c" }, result) end) end) describe("register custom search", function() it("captures git uris", function() search.git = search_helpers.generate_custom_search({ capture = "git@[^%s]+%.git", }) local content = [[ git@github.com:axieax/urlview.nvim.git git@github.com:axieax/typo.nvim.git ]] local links = search.git({ content = content }) assert_tbl_same_any_order({ "git@github.com:axieax/urlview.nvim.git", "git@github.com:axieax/typo.nvim.git", }, links) search.git = nil end) it("captures ssh uris iwthout the prefix", function() search.ssh = search_helpers.generate_custom_search({ capture = "ssh://([^%s]+)", }) local content = [[ ssh://git@github.com:axieax/urlview.nvim.git ssh://git@github.com:axieax/typo.nvim.git ssh://192.168.1.1 ssh://192.168.1.1:4000 ]] local links = search.ssh({ content = content }) assert_tbl_same_any_order({ "git@github.com:axieax/urlview.nvim.git", "git@github.com:axieax/typo.nvim.git", "192.168.1.1", "192.168.1.1:4000", }, links) search.ssh = nil end) it("captures ftp uris", function() search.ftp = search_helpers.generate_custom_search({ capture = "ftp://[^%s]+", }) local content = [[ ftp://ftp.example.com ftp://user@host/%2Ffoo/bar.txt ftp://user:password@server/pathname;type=a ]] local links = search.ftp({ content = content }) assert_tbl_same_any_order({ "ftp://ftp.example.com", "ftp://user@host/%2Ffoo/bar.txt", "ftp://user:password@server/pathname;type=a", }, links) search.ftp = nil end) end) ================================================ FILE: tests/urlview/capture_jump_spec.lua ================================================ local jump = require("urlview.jump") local jump_helpers = require("tests.urlview.jump_helpers") local set_cursor = jump_helpers.set_cursor local create_buffer = jump_helpers.create_buffer local teardown_windows = jump_helpers.teardown_windows local jump_forwards = jump_helpers.jump_forwards local jump_backwards = jump_helpers.jump_backwards -- ASSUMPTION(cursor_pos): line numbers are 1-indexed, column numbers are 0-indexed local examples = { empty = "", invalid = "hello", single_line_middle = "abc https://www.google.com def", standard_url = "https://www.google.com", multi_line_just_links = [[ https://www.google.com https://www.github.com https://www.amazon.com https://www.reddit.com]], multi_line_sandwich = [[ https://www.google.com ]], } describe("line_match_positions unit tests", function() it("multiple substrings", function() local line = "abc abc abc" local expected_no_offset = { 0, 4, 8 } for offset = 0, 100 do local res = jump.line_match_positions(line, "abc", offset) local expected = vim.tbl_map(function(x) return x + offset end, expected_no_offset) vim.deep_equal(expected, res) end end) it("single URL", function() local url = examples.standard_url local res = jump.line_match_positions(url, url, 0) vim.deep_equal({ 0 }, res) end) it("correct single index", function() local url = examples.standard_url local line = examples.single_line_middle local res = jump.line_match_positions(line, url, 0) vim.deep_equal({ 4 }, res) end) end) describe("correct starting column", function() it("backwards no URL", function() local line = examples.invalid create_buffer(line) for col = 0, #line do local new_col = jump.correct_start_col(1, col, true) assert.equals(col, new_col) end end) it("backwards before URL", function() local line = examples.single_line_middle create_buffer(line) local url_start = 4 for col = 0, url_start - 1 do local new_col = jump.correct_start_col(1, col, true) assert.equals(col, new_col) end end) it("backwards on start of URL", function() local line = examples.single_line_middle local url_start = 4 create_buffer(line) local new_col = jump.correct_start_col(1, url_start, true) assert.equals(url_start - 1, new_col) end) it("backwards on rest of URL", function() local line = examples.single_line_middle create_buffer(line) local url_start = 4 local url_end = url_start + #examples.standard_url for col = url_start + 1, url_end - 1 do local new_col = jump.correct_start_col(1, col, true) assert.equals(url_end, new_col) end end) it("backwards after URL", function() local line = examples.single_line_middle local url_start = 4 local url_end = url_start + #examples.standard_url for col = url_end, #line do local new_col = jump.correct_start_col(1, col, true) assert.equals(col, new_col) end end) it("forwards no URL", function() local line = examples.invalid create_buffer(line) for col = 0, #line do local new_col = jump.correct_start_col(1, col, false) assert.equals(col, new_col) end end) it("forwards before URL", function() local line = examples.single_line_middle create_buffer(line) local url_start = 4 for col = 0, url_start - 1 do local new_col = jump.correct_start_col(1, col, false) assert.equals(col, new_col) end end) it("forwards on URL", function() local line = examples.single_line_middle create_buffer(line) local url_start = 4 local url_end = url_start + #examples.standard_url for col = url_start, url_end - 1 do local new_col = jump.correct_start_col(1, col, false) assert.equals(url_end, new_col) end end) it("forwards after URL", function() local line = examples.single_line_middle local url_start = 4 local url_end = url_start + #examples.standard_url for col = url_end, #line do local new_col = jump.correct_start_col(1, col, false) assert.equals(col, new_col) end end) end) describe("backwards jump", function() after_each(teardown_windows) it("empty buffer", function() local content = examples.empty create_buffer(content, { 1, 0 }) local res = jump_backwards() assert.is_nil(res) end) it("no URL", function() local content = examples.invalid create_buffer(content) for col = 0, #content do set_cursor({ 1, col }) local res = jump_backwards() assert.is_nil(res) end end) it("just URL", function() local content = examples.standard_url create_buffer(content, { 1, 0 }) local res = jump_backwards() assert.is_nil(res) for col = 1, #content do set_cursor({ 1, col }) res = jump_backwards() vim.deep_equal({ 1, 0 }, res) end end) it("invalid jump before URL and start of URL", function() local content = examples.single_line_middle local url_start = 4 create_buffer(content) for col = 0, url_start do set_cursor({ 1, col }) local res = jump_backwards() assert.is_nil(res) end end) it("simple jump to start of URL", function() local content = examples.single_line_middle local url_start = 4 create_buffer(content) for col = url_start + 1, #content do set_cursor({ 1, col }) local res = jump_backwards() vim.deep_equal({ 1, url_start }, res) end end) it("multiline jump from anywhere in line", function() local content = examples.multi_line_just_links local url_length = #examples.standard_url create_buffer(content) -- invalid jump set_cursor({ 1, 0 }) local res = jump_backwards() assert.is_nil(res) -- jump to start of previous URL local line_last = 4 for line = 1, line_last do local expected = { line, 0 } for col = 1, url_length do set_cursor({ line, col }) res = jump_backwards() vim.deep_equal(expected, res) end if line ~= line_last then set_cursor({ line + 1, 0 }) vim.deep_equal(expected, res) end end end) it("multiline sandwich", function() local content = examples.multi_line_sandwich local url_length = #examples.standard_url create_buffer(content) -- invalid jumps for line = 1, 2 do set_cursor({ line, 0 }) local res = jump_backwards() assert.is_nil(res) end -- jumps to correct position local expected = { 2, 0 } for col = 1, url_length do set_cursor({ 2, col }) local res = jump_backwards() vim.deep_equal(expected, res) end -- line 3 jumps to line 2 set_cursor({ 3, 0 }) local res = jump_backwards() vim.deep_equal(expected, res) end) it("jump chain", function() local content = examples.multi_line_just_links local url_length = #examples.standard_url create_buffer(content, { 4, url_length }) for line = 4, 1, -1 do local res = jump_backwards() vim.deep_equal({ line, 0 }, res) end local res = jump_backwards() assert.is_nil(res) end) end) describe("forwards jump", function() after_each(teardown_windows) it("empty buffer", function() local content = examples.empty create_buffer(content, { 1, 0 }) local res = jump_forwards() assert.is_nil(res) end) it("no URL", function() local content = examples.invalid create_buffer(content) for col = 0, #content do set_cursor({ 1, col }) local res = jump_forwards() assert.is_nil(res) end end) it("just URL", function() local content = examples.standard_url create_buffer(content) for col = 0, #content do set_cursor({ 1, col }) local res = jump_forwards() assert.is_nil(res) end end) it("same line single", function() local content = examples.single_line_middle local url_start = 4 create_buffer(content) for col = 0, url_start - 1 do set_cursor({ 1, col }) local res = jump_forwards() vim.deep_equal({ 1, url_start }, res) end end) it("on last URL + no more", function() local content = examples.single_line_middle local url_start = 4 create_buffer(content) for col = url_start, #content do set_cursor({ 1, col }) local res = jump_forwards() assert.is_nil(res) end end) it("multiline jump from anywhere in line", function() local content = examples.multi_line_just_links local url_length = #examples.standard_url create_buffer(content) -- jumps to next line for line = 1, 3 do for col = 0, url_length do set_cursor({ line, col }) local res = jump_forwards() vim.deep_equal({ line + 1, 0 }, res) end end -- last line for col = 0, url_length do set_cursor({ 4, col }) local res = jump_forwards() assert.is_nil(res) end end) it("multiline sandwich", function() local content = examples.multi_line_sandwich local url_length = #examples.standard_url create_buffer(content, { 1, 0 }) -- line 1 jumps to line 2 local res = jump_forwards() vim.deep_equal({ 2, 0 }, res) -- line 2 onwards invalid for col = 0, url_length do set_cursor({ 2, col }) res = jump_forwards() assert.is_nil(res) end set_cursor({ 3, 0 }) res = jump_forwards() assert.is_nil(res) end) it("jump chain", function() local content = examples.multi_line_just_links create_buffer(content, { 1, 0 }) for line = 1, 3 do local res = jump_forwards() vim.deep_equal({ line + 1, 0 }, res) end local res = jump_forwards() assert.is_nil(res) end) end) ================================================ FILE: tests/urlview/capture_mock_spec.lua ================================================ local urlview = require("urlview") local reset_config = require("urlview.config.helpers").reset_defaults local search = require("urlview.search") local search_helpers = require("urlview.search.helpers") describe("mock vim.ui.select", function() local default_prefix = "test-" before_each(function() urlview.setup({ default_prefix = default_prefix }) search.test = search_helpers.generate_custom_search({ capture = "%w+", format = "%s", }) assert.is_not.Nil(search.test) end) after_each(function() search.test = nil reset_config() end) local original_ui_select = vim.ui.select it("unique, sorted", function() local content = "pears watermelon banana apple apple peach apricot watermelon" local expected = vim.tbl_map(function(v) return default_prefix .. v end, { "apple", "apricot", "banana", "peach", "pears", "watermelon" }) vim.ui.select = function(items) assert.same(expected, items) end urlview.search("test", { content = content, unique = true, sorted = true }) vim.ui.select = original_ui_select end) end) describe("mock telescope", function() end) ================================================ FILE: tests/urlview/capture_multiple_spec.lua ================================================ local urlview = require("urlview") local extract_links_from_content = require("urlview.search.helpers").content local reset_config = require("urlview.config.helpers").reset_defaults local assert_tbl_same_any_order = require("tests.urlview.helpers").assert_tbl_same_any_order describe("multiple captures", function() it("separate lines", function() local content = [[ http://google.com https://www.google.com ]] local result = extract_links_from_content(content) assert_tbl_same_any_order({ "http://google.com", "https://www.google.com" }, result) end) it("same line", function() local content = "http://google.com https://www.github.com" local result = extract_links_from_content(content) assert_tbl_same_any_order({ "http://google.com", "https://www.github.com" }, result) end) end) describe("unique captures", function() it("same link", function() local content = [[ http://google.com http://google.com ]] local result = extract_links_from_content(content) assert_tbl_same_any_order({ "http://google.com" }, result) end) it("different prefix / uri protocol, same default prefix", function() urlview.setup({ default_prefix = "https://", }) local content = [[ https://www.google.com www.google.com ]] local result = extract_links_from_content(content) assert_tbl_same_any_order({ "https://www.google.com" }, result) reset_config() end) it("different prefix / uri protocol, prefer specified", function() urlview.setup({ default_prefix = "http://", }) local content = [[ https://www.google.com www.google.com ]] local result = extract_links_from_content(content) assert_tbl_same_any_order({ "https://www.google.com" }, result) reset_config() end) it("different paths", function() local content = [[ https://www.google.com/search?q=vim https://www.google.com/search?q=nvim ]] local result = extract_links_from_content(content) assert_tbl_same_any_order({ "https://www.google.com/search?q=vim", "https://www.google.com/search?q=nvim" }, result) end) end) ================================================ FILE: tests/urlview/capture_process_spec.lua ================================================ local urlview = require("urlview") local search = require("urlview.search") local search_helpers = require("urlview.search.helpers") local reset_config = require("urlview.config.helpers").reset_defaults local assert_tbl_same_any_order = require("tests.urlview.helpers").assert_tbl_same_any_order local prepare_links = require("urlview.utils").process_links local extract_links_from_content = search_helpers.content describe("HTTP(s) protocol fill in", function() local default_prefix = "https://" before_each(function() urlview.setup({ default_prefix = default_prefix }) end) after_each(function() reset_config() end) it("link with http protocol", function() local url = "http://example.com" local links = extract_links_from_content(url) local prepared_links = prepare_links(links) assert.same({ url }, prepared_links) end) it("link with https protocol", function() local url = "https://example.com" local links = extract_links_from_content(url) local prepared_links = prepare_links(links) assert.same({ url }, prepared_links) end) it("link missing either protocol", function() local url = "www.example.com" local links = extract_links_from_content(url) local prepared_links = prepare_links(links) assert.same({ default_prefix .. url }, prepared_links) end) end) describe("unique links", function() before_each(function() urlview.setup({ default_prefix = "", }) search.test = search_helpers.generate_custom_search({ capture = "%d", format = "%s", }) assert.is_not.Nil(search.test) end) after_each(function() search.test = nil reset_config() end) local content = "1 1 2 2 3 3 4 4 5 5" it("keep duplicates", function() local links = search.test({ content = content }) local prepared_links = prepare_links(links, { unique = false }) assert_tbl_same_any_order({ "1", "1", "2", "2", "3", "3", "4", "4", "5", "5" }, prepared_links) end) it("filter duplicates", function() local links = search.test({ content = content }) local prepared_links = prepare_links(links, { unique = true }) assert.same({ "1", "2", "3", "4", "5" }, prepared_links) end) end) describe("sorted links", function() local default_prefix = "https://" before_each(function() urlview.setup({ default_prefix = default_prefix, sort = true }) end) after_each(function() reset_config() end) it("URLs missing protocol fixed and sorted alphabetically", function() local content = [[ www.google.com https://google.com https://github.com/axieax/urlview.nvim www.example.com http://github.com/helloM http://github.com/helloP http://github.com/hellon ]] local links = extract_links_from_content(content) local prepared_links = prepare_links(links) local expected = { "http://github.com/helloM", "http://github.com/hellon", "http://github.com/helloP", "https://github.com/axieax/urlview.nvim", "https://google.com", default_prefix .. "www.example.com", default_prefix .. "www.google.com", } assert.same(expected, prepared_links) end) end) ================================================ FILE: tests/urlview/capture_single_spec.lua ================================================ local urlview = require("urlview") local reset_config = require("urlview.config.helpers").reset_defaults local assert_no_match = require("tests.urlview.helpers").assert_no_match local assert_single_match = require("tests.urlview.helpers").assert_single_match describe("no capture", function() it("empty string", function() assert_no_match("") end) it("random", function() assert_no_match("asdfwiueyfksdlckvj") end) it("com only", function() assert_no_match("test.com") end) it("com path", function() assert_no_match("test.com/idk") end) end) describe("url-only simple capture", function() local default_prefix = "https://" before_each(function() urlview.setup({ default_prefix = default_prefix, }) end) after_each(function() reset_config() end) it("http capture", function() local url = "http://google.com" assert_single_match(url, url) end) it("https capture", function() local url = "https://google.com" assert_single_match(url, url) end) it("www capture", function() local url = "www.google.com" assert_single_match(url, url) end) it("http www capture", function() local url = "http://www.google.com" assert_single_match(url, url) end) it("https www capture", function() local url = "https://www.google.com" assert_single_match(url, url) end) it("trailing slash", function() local url = "www.google.com/" assert_single_match(url, url) end) end) describe("url-only path capture", function() local default_prefix = "http://" before_each(function() urlview.setup({ default_prefix = default_prefix, }) end) after_each(function() reset_config() end) it("lol php capture", function() local url = "https://who.even.uses/index.php" assert_single_match(url, url) end) it("https path capture", function() local url = "https://google.com/path/to/idk" assert_single_match(url, url) end) it("www path capture", function() local url = "www.google.com/path/to/idk" assert_single_match(url, url) end) it("url-encoded path query capture", function() local url = "www.google.com/P%40%2Bh%20t35T%2F/1dk%3F?q=%3Da%25%3B" assert_single_match(url, url) end) it("query capture", function() local url = "https://example.com/path/to/idk?q=axie&ax" assert_single_match(url, url) end) end) ================================================ FILE: tests/urlview/helpers.lua ================================================ local M = {} local extract_links_from_content = require("urlview.search.helpers").content function M.assert_no_match(content) local result = extract_links_from_content(content) assert.equals(0, #result) end function M.assert_single_match(url, expected_url) local result = extract_links_from_content(url) assert.same({ expected_url }, result) end function M.assert_tbl_same_any_order(expected, actual) assert.same(#expected, #actual) for _, e in ipairs(expected) do assert.truthy(vim.tbl_contains(actual, e)) end end return M ================================================ FILE: tests/urlview/jump_helpers.lua ================================================ local M = {} local jump = require("urlview.jump") local utils = require("urlview.utils") local active_windows = {} function M.set_cursor(pos) vim.api.nvim_win_get_cursor = function() return pos end end function M.create_buffer(content, cursor_pos) local lines = vim.split(content, "\n") local bufnr = vim.api.nvim_create_buf(false, true) vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) local winnr = vim.api.nvim_open_win(bufnr, false, { relative = "editor", row = 0, col = 0, width = 1, height = 1, zindex = 1, }) active_windows[winnr] = true vim.fn.getline = function(line_no) return lines[line_no] end vim.api.nvim_buf_line_count = function() return #lines end utils.fallback(cursor_pos, { 1, 0 }) M.set_cursor(cursor_pos) end function M.teardown_windows() local windows = vim.tbl_keys(active_windows) for _, winnr in ipairs(windows) do if vim.api.nvim_win_is_valid(winnr) then vim.api.nvim_win_close(winnr, true) active_windows[winnr] = nil end end end function M.jump_forwards() local res = jump.find_url(0, false) if res then M.set_cursor(res) end return res end function M.jump_backwards() local res = jump.find_url(0, true) if res then M.set_cursor(res) end return res end return M ================================================ FILE: vim.toml ================================================ # SOURCE: https://github.com/jose-elias-alvarez/null-ls.nvim/blob/main/vim.toml [selene] base = "lua51" name = "vim" [vim] any = true [[describe.args]] type = "string" [[describe.args]] type = "function" [[it.args]] type = "string" [[it.args]] type = "function" [[before_each.args]] type = "function" [[after_each.args]] type = "function" [assert.is_not] any = true [assert.matches] any = true [assert.has_error] any = true [[assert.equals.args]] type = "any" [[assert.equals.args]] type = "any" [[assert.equals.args]] type = "any" required = false [[assert.same.args]] type = "any" [[assert.same.args]] type = "any" [[assert.truthy.args]] type = "any" [[assert.falsy.args]] type = "any" [[assert.spy.args]] type = "any" [[assert.stub.args]] type = "any" [[assert.is_nil.args]] type = "any"