[
  {
    "path": ".busted",
    "content": "return {\n  _all = {\n    coverage = false,\n    lpath = \"lua/?.lua;lua/?/init.lua\",\n  },\n  default = {\n    verbose = true,\n  },\n  tests = {\n    verbose = true,\n  },\n}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: LuaRocks release\non:\n  push:\n    tags: # Will upload to luarocks.org when a tag is pushed\n      - \"*\"\n  pull_request: # Will test a local install without uploading to luarocks.org\n\njobs:\n  luarocks-release:\n    runs-on: ubuntu-latest\n    name: LuaRocks upload\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n      - name: LuaRocks Upload\n        uses: nvim-neorocks/luarocks-tag-release@v7\n        env:\n          LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Compiled Lua sources\nluac.out\n\n# luarocks build files\n*.src.rock\n*.zip\n*.tar.gz\n\n# Object files\n*.o\n*.os\n*.ko\n*.obj\n*.elf\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Libraries\n*.lib\n*.a\n*.la\n*.lo\n*.def\n*.exp\n\n# Shared objects (inc. Windows DLLs)\n*.dll\n*.so\n*.so.*\n*.dylib\n\n# Executables\n*.exe\n*.out\n*.app\n*.i*86\n*.x86_64\n*.hex\n\nlua/.luarc.json\n\n/luarocks\n/lua_modules\n/.luarocks\n\n.nvimlog\n\n# Temporary files for testing\nimage\nimages\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## v2.0.0\n\n### Added\n\n* Features: Hide/reveal detours and uncover hidden base windows\n* Implement `CloseCurrentStack`\n* A default title to all detours based on buffer name\n    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/6f7a718e1ea0d24daff16407b27e460e043ebf6f)\n* This changelog\n* Added help pages\n\n### Fixed\n\n* Keep cursor out of covered windows\n    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/0c358da951addace23934db10df59cc609e81db4)\n* Check window exists before updating its title\n    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/6cd2b457e4a5502cdaaf510a3da66d2686d42cc9)\n* Fix global statusline detection\n    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/f452858a3bac44bdabb9f507ba219e3e0af4bc6c)\n* Attempt to fix terminal rendering issue\n    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/b5596b9baa61475fe5164142c7d8ca86d0cf3b37)\n* Miscellaneous small fixes\n    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/eaab89288dd14de8d7cd06a948589b8f439c12ad)\n* Make updating title more responsive\n    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/255fd9555d389d21a3bf790de47a2350b5607bf5)\n* Guard title update autocmd\n    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/0e206f5aacf9f65b2d92cc9098519a7ea3595536)\n* Realigned terminal buffers after resize events\n    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/a7935ce1283a141bcca09d6bdf07c9c1b537bbfb)\n* Redid help detour example\n    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/bf59c29a06b58cd0e9f53b04aad7646204af4479)\n* Introduced nesting autocmds\n    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/42a724730e2351057973e1231016b8918e161e4f)\n\n### Changed\n\n* Increase required Neovim version to `0.11`\n    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/def7b8c2e7b930c1d9f807f4362e61fb8796f11e)\n* Removed behavior that closes detour if its parents are closed.\n    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/48d6e7031007f4ebda460b99beeecc50ef932bcc)\n* Keep window sizing behavior consistent until very small sizes\n    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/39b19018711073edb0dd69a790e2ffdb4ebeb50c)\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM alpine:3.22\n\nENV LUA_MAJOR_VERSION 5.1\nENV LUA_MINOR_VERSION 5\nENV LUA_VERSION ${LUA_MAJOR_VERSION}.${LUA_MINOR_VERSION}\n\n# Dependencies\nRUN apk update && apk add --update make tar unzip gcc openssl-dev readline-dev curl libc-dev\nRUN apk add wget # Needed due to https://github.com/luarocks/luarocks/issues/952\n\nRUN curl -L http://www.lua.org/ftp/lua-${LUA_VERSION}.tar.gz | tar xzf -\nWORKDIR /lua-$LUA_VERSION\n\n# build lua\nRUN make linux test\nRUN make install\n\nWORKDIR /\n\n# lua env\nENV WITH_LUA /usr/local/\nENV LUA_LIB /usr/local/lib/lua\nENV LUA_INCLUDE /usr/local/include\n\n\nRUN rm /lua-$LUA_VERSION -rf\n\nENV LUAROCKS_VERSION 3.9.2\nENV LUAROCKS_INSTALL luarocks-$LUAROCKS_VERSION\nENV TMP_LOC /tmp/luarocks\n\n# Build Luarocks\nRUN curl -OL \\\n    https://luarocks.org/releases/${LUAROCKS_INSTALL}.tar.gz\n\nRUN tar xzf $LUAROCKS_INSTALL.tar.gz && \\\n    mv $LUAROCKS_INSTALL $TMP_LOC && \\\n    rm $LUAROCKS_INSTALL.tar.gz\n\n\nWORKDIR $TMP_LOC\n\nRUN ./configure \\\n  --with-lua=$WITH_LUA \\\n  --with-lua-include=$LUA_INCLUDE \\\n  --with-lua-lib=$LUA_LIB\n\nRUN make build\n\nRUN make install\n\nWORKDIR /\n\nRUN rm $TMP_LOC -rf\n\nWORKDIR /mnt/luarocks\n\nRUN apk add 'neovim=0.11.1-r1'\nENV BUSTED_VERSION 2.1.2-3\nRUN luarocks install busted $BUSTED_VERSION\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Roger Kim\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": "DOCKER_IMAGE = detour_tester\n\n.PHONY : test\n\nimage: Dockerfile\n\tdocker build -t ${DOCKER_IMAGE} .\n\ttouch image\n\ntest: image\n\tdocker run --volume=\"$(shell pwd):/mnt/luarocks:Z\" ${DOCKER_IMAGE} /mnt/luarocks/run-in-docker.sh\n\n.PHONY: help\n# Ensure init.lua is processed first. Exclude internal helpers from docs.\nHELP_LUA_ALL := $(wildcard lua/detour/*.lua)\n# Space-separated list of files to exclude from lemmy-help\nHELP_EXCLUDE := lua/detour/internal.lua \\\n                lua/detour/windowing_algorithm.lua \\\n                lua/detour/config.lua \\\n                lua/detour/util.lua \\\n                lua/detour/show_path_in_title.lua\nHELP_LUA_SRCS := $(filter-out $(HELP_EXCLUDE),$(HELP_LUA_ALL))\n# Ensure both init.lua and features.lua are first in order\nHELP_LUA_HEAD := lua/detour/init.lua lua/detour/algorithm_doc.lua lua/detour/movements.lua\nHELP_LUA_TAIL := $(filter-out $(HELP_LUA_HEAD),$(HELP_LUA_SRCS))\nhelp:\n\t# Generate help docs via lemmy-help from Lua sources (with excludes)\n\t@mkdir -p doc\n\tlemmy-help $(HELP_LUA_HEAD) $(HELP_LUA_TAIL) > doc/detour.txt\n\t# Rebuild helptags (optional)\n\t@nvim --headless -c 'silent! helptags doc' -c 'q' || true\n"
  },
  {
    "path": "README.md",
    "content": "# detour.nvim\n\n> It's a dangerous business, Frodo, going out your door. You step onto the road, and if you don't keep your feet, there's no knowing where you might be swept off to.\n\n<div dir=\"rtl\">\nJ.R.R. Tolkien, The Lord of the Rings\n</div>\n</br></br>\n\n`detour.nvim` provides commands to open floating windows that position and shape\nthemselves.\n\n# Never lose your spot!📍🗺️\n\n| What does detour.nvim do? | |\n| :--: | :--: |\n| `:Detour`/`require('detour').Detour()` <br />opens a floating window<br />over all current windows | ![detour](https://github.com/carbon-steel/detour.nvim/assets/7697639/1eb85155-7134-473f-8df0-dd15f55c1d8c) |\n| `:DetourCurrentWindow`/<br />`require('detour').DetourCurrentWindow()`<br />opens a floating window over<br />only the current window | ![detour2](https://github.com/carbon-steel/detour.nvim/assets/7697639/d3f0db15-916b-4b17-b227-0e4aa8fc318d) |\n| Works with Neovim's `:split`/`:vsplit`/`<C-w>s`/`<C-w>v`/`<C-w>T` commands | ![split](https://github.com/carbon-steel/detour.nvim/assets/7697639/4ffa7f36-8b2a-4d91-a8bb-7012f7b82015) |\n| You can nest detour popups | ![nest](https://github.com/carbon-steel/detour.nvim/assets/7697639/5fc3cad6-9acf-482d-97cb-c75788617cf8) |\n\nNeovim's floating windows are a great utility to use in plugins and\nfunctions, but they cannot be used manually. This is because\ncreating floats is not simple like calling `:split` or `:vsplit`.\n`vim.api.nvim_open_win(...)` requires coordinates and dimensions to make\na float which is too tedious to do by hand.\n\nDetour.nvim brings a single new feature to Neovim: **detour windows** (aka\ndetours). Detours are floating windows with the ease-of-use of splits.\n\nDetours will make sure not to overlap each other unless when a detour is\nnested within another.\n\n# Example keymaps\n\n`detour.nvim` is designed as a utility library for keymaps people can write on their own.\n\n**NOTE** If you'd like to share a keymap you made, please submit it in a github issue and we'll include it in the `examples` directory!\n\nHere are a few basic examples...\n\n### Open your Neovim config\n\n```lua\nvim.keymap.set(\"n\", \"<leader>e\", function()\n\t-- Open detour\n\tif not require(\"detour\").Detour() then\n\t\treturn\n\tend\n\tvim.cmd.edit(vim.fn.stdpath(\"config\")) -- open Neovim config directory\nend)\n```\n\n[Screencast from 2025-09-11 07-37-45.webm](https://github.com/user-attachments/assets/9842dde2-3c42-4ade-aa27-35d23b45b42c)\n\n### Jump to definition in detour\n\n```lua\nvim.keymap.set(\"n\", \"<leader>gd\", function()\n\t-- Open detour with the same buffer\n\tif not require(\"detour\").Detour() then\n\t\treturn\n\tend\n\tvim.lsp.buf.definition() -- jump to definition\nend)\n```\n\n[Screencast from 2025-09-13 18-57-09.webm](https://github.com/user-attachments/assets/5f71ace1-1392-4082-8daa-83be88669324)\n\n### Wrap a TUI: top\n\nYou can wrap any TUI in a detour. Here is an example.\n\nRun `top` in a detour:\n\n```lua\nvim.keymap.set(\"n\", \"<leader>p\", function()\n\tlocal window_id = require(\"detour\").Detour() -- open a detour\n\tif not window_id then\n\t\treturn\n\tend\n\n\tvim.cmd.terminal(\"top\") -- open a terminal buffer\n\tvim.bo.bufhidden = \"delete\" -- close the terminal when window closes\n\tvim.wo[window_id].signcolumn = \"no\" -- In Neovim 0.10, the signcolumn can push the TUI a bit out of window\n\n\t-- It's common for people to have `<Esc>` mapped to `<C-\\><C-n>` for terminals.\n\t-- This can get in the way when interacting with TUIs.\n\t-- This maps the escape key back to itself (for this buffer) to fix this problem.\n\tvim.keymap.set(\"t\", \"<Esc>\", \"<Esc>\", { buffer = true })\n\n\tvim.cmd.startinsert() -- go into insert mode\n\n\tvim.api.nvim_create_autocmd({ \"TermClose\" }, {\n\t\tbuffer = vim.api.nvim_get_current_buf(),\n\t\tcallback = function()\n\t\t\t-- This automated keypress skips for you the \"[Process exited 0]\" message\n\t\t\t-- that the embedded terminal shows.\n\t\t\tvim.api.nvim_feedkeys(\"i\", \"n\", false)\n\t\tend,\n\t})\nend)\n```\n\n||\n| :--: |\n| **Use keymap above -> Close window** |\n![top](https://github.com/carbon-steel/detour.nvim/assets/7697639/49dd12ab-630b-4558-9486-fe82cc94882c)\n\n# Installation\n\n### Lazy.nvim\n\n```lua\n{ \"carbon-steel/detour.nvim\",\n    config = function ()\n        require(\"detour\").setup({\n            -- Put custom configuration here\n        })\n        vim.keymap.set('n', '<c-w><enter>', \":Detour<cr>\")\n        vim.keymap.set('n', '<c-w>.', \":DetourCurrentWindow<cr>\")\n\n        local detour_moves = require(\"detour.movements\")\n        -- NOTE: While using `detour_moves` is not required to use this\n        -- plugin, it is strongly recommended as it makes window navigation\n        -- much more intuitive.\n        --\n        -- The following keymaps are drop in replacements for Vim's regular\n        -- window navigation commands. These replacements allows you to\n        -- skip over windows covered by detours (which is a much more\n        -- intuitive motion) but are otherwise the same as normal window\n        -- navigation.\n        --\n        -- This is an example set of keymaps, but if you use other keys to\n        -- navigate windows, changes these keymaps to suit your situation.\n        vim.keymap.set({ \"n\", \"t\" }, \"<C-j>\", detour_moves.DetourWinCmdJ)\n        vim.keymap.set({ \"n\", \"t\" }, \"<C-w>j\", detour_moves.DetourWinCmdJ)\n        vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-j>\", detour_moves.DetourWinCmdJ)\n\n        vim.keymap.set({ \"n\", \"t\" }, \"<C-h>\", detour_moves.DetourWinCmdH)\n        vim.keymap.set({ \"n\", \"t\" }, \"<C-w>h\", detour_moves.DetourWinCmdH)\n        vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-h>\", detour_moves.DetourWinCmdH)\n\n        vim.keymap.set({ \"n\", \"t\" }, \"<C-k>\", detour_moves.DetourWinCmdK)\n        vim.keymap.set({ \"n\", \"t\" }, \"<C-w>k\", detour_moves.DetourWinCmdK)\n        vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-k>\", detour_moves.DetourWinCmdK)\n\n        vim.keymap.set({ \"n\", \"t\" }, \"<C-l>\", detour_moves.DetourWinCmdL)\n        vim.keymap.set({ \"n\", \"t\" }, \"<C-w>l\", detour_moves.DetourWinCmdL)\n        vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-l>\", detour_moves.DetourWinCmdL)\n\n        vim.keymap.set({ \"n\", \"t\" }, \"<C-w>w\", detour_moves.DetourWinCmdW)\n        vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-w>\", detour_moves.DetourWinCmdW)\n\n    end\n},\n```\n\n# Options\n\n| Option  | Description                                                                                 | Default value |\n| --      | --                                                                                          | --            |\n| `title` | \"path\" sets the path of the current buffer as the title of the float. \"none\" sets no title. | \"path\"        |\n\n# Advanced\n\nUsing detours can be simple, but they also come with features for power users:\n\n* `require(\"detour.features\").UncoverWindow`: Update all detours to uncover a\n  given regular window\n* `require(\"detour.features\").UncoverWindowWithMouse`: Same as above but select\n  which window to uncover with the mouse\n* `require(\"detour.features\").HideAllDetours`/`RevealAllDetours`: Hide and\n  reveal all detours so you can see the windows behind them\n* `require(\"detour.features\").CloseCurrentStack`: Closes the current detour\n  along with all detours it is nested within\n\n# Development\n\n* Build help docs: run `make help` from the repo root.\n    - Requires `lemmy-help` in your PATH ([repo](https://github.com/numToStr/lemmy-help/tree/master)).\n    - Optional: Neovim available for generating `helptags` (target does not fail if absent).\n* Run test: run `make test` from the repo root\n    - Requires `docker`\n\n# FAQ\n\n> I want to convert detours to splits or tabs.\n\n`<C-w>s` and `<C-w>v` can be used from within a popup to create splits. `<C-w>T` creates tabs.\n\n> My LSP keeps moving my cursor to other windows.\n\nIf your LSP movements (ex: `go-to-definition`) are opening locations in other windows, make sure that `reuse_win` is set to `false`.\n\n> My floating windows don't look good.\n\nSome colorschemes don't have visually clear floating window border colors. Consider customizing your colorscheme's FloatBorder to a color that makes your floating windows clearer.\n\n> My TUI is slightly wider than the floating window it's in.\n\nThis is something I noticed happening when I upgraded to Neovim 0.10. After you create your detour floating window, make sure to turn off `signcolumn`.\n\n```lua\nvim.opt.signcolumn = \"no\"\n```\n"
  },
  {
    "path": "detour.nvim-scm-1.rockspec",
    "content": "rockspec_format = '3.0'\npackage = 'detour.nvim'\nversion = 'scm-1'\n\ntest_dependencies = {\n  'lua >= 5.1',\n}\n\nsource = {\n  url = 'git://github.com/carbon-steel/' .. package,\n}\n\nbuild = {\n  type = 'builtin',\n}\n"
  },
  {
    "path": "doc/detour.txt",
    "content": "==============================================================================\n                                                                        *detour*\n\n                                                                   *detour-into*\nNeovim/Vim's floating windows are a great utility to use in plugins and\nfunctions, but they cannot be used manually as splits are. This is because\ncreating floats is not simple like calling `:split` or `:vsplit`.\n`vim.api.nvim_open_win(...)` requires coordinates and dimensions to make\na float which is too tedious to do by hand.\n\nDetour.nvim brings a single new feature to Neovim: detour windows (aka\ndetours). Detours are floating windows with the ease-of-use of splits.\n\nThey dynamically shape themselves to cover as much of a given area as possible.\nThey can cover:\n* the whole screen (`require(\"detour\").Detour()`)\n* the current window (`require(\"detour\").DetourCurrentWindow()`)\n* the current detour (both of the above functions would work)\n\nDetours will make sure not to overlap each other unless when a detour is\nnested within another.\n\nYou will find that there are many cases where using a large floating window is\npreferable to creating a smaller split window. On top of that, the nesting\nbehavior of detours allows you to take a long \"detour\" into other files/locations\nwithout losing your place in your regular windows. Take a detour, look at\nother locations, close the detour, and find your original windows as they\nwere when you left.\n\ndetour.Detour()                                                  *detour.Detour*\n    Open a new detour window\n\n    * If this is called from a non-detour window, the largest possible detour\n    window will be opened that does not overlap with any other detours.\n    * If this is called from a detour window, a detour will be opened nested\n    within just the current detour.\n\n    There are cases where there is no space for a new detour window and\n    this function call will do nothing.\n\n    Returns: ~\n        (integer|nil)  returns detour's window id if successfully\n                       created, nil otherwise\n                       @nodiscard\n\n    Usage: ~\n>lua\n\n        --- local window_id = require(\"detour\").Detour()\n        --- if not window_id then\n        ---     -- New detour could not be made so stop execution\n        ---     return\n        --- end\n        ---\n        --- -- New detour window is open and cursor is moved to it.\n<\n\n\ndetour.DetourCurrentWindow()                        *detour.DetourCurrentWindow*\n    Open a detour popup covering only the current window.\n\n    There are cases where there is no space for a new detour window and\n    this function call will do nothing.\n\n    Returns: ~\n        (integer|nil)  returns detour's window id if successfully\n                       created, nil otherwise\n                       @nodiscard\n\n    Usage: ~\n>lua\n\n        --- local window_id = require(\"detour\").DetourCurrentWindow()\n        --- if not window_id then\n        --- \t-- New detour could not be made so stop execution\n        --- \treturn\n        --- end\n        ---\n        --- -- New detour window is open and cursor is moved to it.\n<\n\n\n==============================================================================\n                                                              *detour.algorithm*\n\n                                                              *detour-algorithm*\nDetour's windowing algorithm computes the largest rectangle where a\nfloating window (detour) can be placed without overlapping windows that must\nremain visible.\n\nOverview\n\n Each detour has a list of \"reserved\" windows that it is allowed to cover.\n\n When creating or resizing a detour, the algorithm will find the largest\n rectangular area where a floating window can go that covers only reserved\n windows. That rectangle will be the position and dimensions of the detour.\n\n `Detour()` creates a detour `d` where all windows that are not currently\n reserved by an existing detour are reserved by `d`.\n\n `DetourCurrentWindow()` creates a detour `d` where only the current window\n is reserved by `d`.\n\nResizing\n\n Whenever windows open, close, or get resized, detours will recalculate the\n largest area they can fill and dynamically reshape themselves. This allows\n them to make room for new windows or to expand to take space that has been\n freed up.\n\n==============================================================================\n                                                              *detour.movements*\n\nDetour.nvim breaks window navigation commands such as\n`<C-w>w`, `<C-w>j`, or `vim.cmd.wincmd(\"h\")`. Instead of\nusing those, the user MUST use the following window navigation\ncommands that this plugin provides that implements moving\nbetween windows while skipping over windows covered by detours.\n\nNOTE: Regular window movements such as `<C-w>w`, `<C-w>j`,\n`vim.cmd.wincmd(\"h\")` should still work in automated\nscripts/functions. Still, you may find it more useful to use detour's\n\"detour-aware\" movement functions in your scripts/functions as well.\n\n                                                              *detour-movements*\nmovements._safe_state_handler()                  *movements._safe_state_handler*\n    DO NOT USE. FOR TESTING ONLY.\n\n\nmovements.DetourWinCmdL()                              *movements.DetourWinCmdL*\n     Switch to a window to the right. Skip over any non-floating windows\n     covered by a detour.\n\n    Returns: ~\n        (nil)\n\n    Usage: ~\n>lua\n\n        --- local detour_moves = require(\"detour.movements\")\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-l>\", detour_moves.DetourWinCmdL)\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-w>l\", detour_moves.DetourWinCmdL)\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-l>\", detour_moves.DetourWinCmdL)\n        ---\n<\n\n\nmovements.DetourWinCmdH()                              *movements.DetourWinCmdH*\n     Switch to a window to the left. Skip over any non-floating windows\n     covered by a detour.\n\n    Returns: ~\n        (nil)\n\n    Usage: ~\n>lua\n\n        --- local detour_moves = require(\"detour.movements\")\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-h>\", detour_moves.DetourWinCmdH)\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-w>h\", detour_moves.DetourWinCmdH)\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-h>\", detour_moves.DetourWinCmdH)\n        ---\n<\n\n\nmovements.DetourWinCmdJ()                              *movements.DetourWinCmdJ*\n     Switch to a window below. Skip over any non-floating windows\n     covered by a detour.\n\n    Returns: ~\n        (nil)\n\n    Usage: ~\n>lua\n\n        --- local detour_moves = require(\"detour.movements\")\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-j>\", detour_moves.DetourWinCmdJ)\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-w>j\", detour_moves.DetourWinCmdJ)\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-j>\", detour_moves.DetourWinCmdJ)\n        ---\n<\n\n\nmovements.DetourWinCmdK()                              *movements.DetourWinCmdK*\n     Switch to a window above. Skip over any non-floating windows\n     covered by a detour.\n\n    Returns: ~\n        (nil)\n\n    Usage: ~\n>lua\n\n        --- local detour_moves = require(\"detour.movements\")\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-k>\", detour_moves.DetourWinCmdK)\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-w>k\", detour_moves.DetourWinCmdK)\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-k>\", detour_moves.DetourWinCmdK)\n        ---\n<\n\n\nmovements.DetourWinCmdW()                              *movements.DetourWinCmdW*\n     Switch windows in a cycle. Skip over any non-floating windows covered\n     by a detour.\n\n    Returns: ~\n        (nil)\n\n    Usage: ~\n>lua\n\n        --- local detour_moves = require(\"detour.movements\")\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-w>\", detour_moves.DetourWinCmdW)\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-w>w\", detour_moves.DetourWinCmdW)\n        --- vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-w>\", detour_moves.DetourWinCmdW)\n        ---\n<\n\n\n==============================================================================\n                                                               *detour.features*\n\n Optional detour.nvim features.\n\n                                                               *detour-features*\nfeatures.ShowPathInTitle({popup_id})                  *features.ShowPathInTitle*\n    Show the buffer path in the given popup's title and keep it updated.\n\n    Parameters: ~\n        {popup_id}  (integer)\n\n    Returns: ~\n        (nil)\n\n\nfeatures.CloseOnLeave({popup_id})                        *features.CloseOnLeave*\n    Close the popup when focus leaves to a non-floating window.\n\n    Parameters: ~\n        {popup_id}  (integer)\n\n    Returns: ~\n        (nil)\n\n\nfeatures.UncoverWindow({window})                        *features.UncoverWindow*\n    Prevent detours from covering the provided window.\n\n    Parameters: ~\n        {window}  (integer)\n\n    Returns: ~\n        (boolean)\n\n\nfeatures.UncoverWindowWithMouse()              *features.UncoverWindowWithMouse*\n    Prompt to click a window and mark it as uncovered by detours.\n\n    Returns: ~\n        (nil)\n\n\nfeatures.HideAllDetours()                              *features.HideAllDetours*\n    Temporarily hide all detours in current tabpage.\n\n    Returns: ~\n        (nil)\n\n\nfeatures.RevealAllDetours()                          *features.RevealAllDetours*\n    Reveal all detours previously hidden in current tabpage.\n\n    Returns: ~\n        (nil)\n\n\nfeatures.CloseCurrentStack()                        *features.CloseCurrentStack*\n     Closes current detour along with all detours that it covers and covers it.\n     When not inside a detour, this function is a no-op.\n\n    Returns: ~\n        (boolean)  true if close operation succeeded and false otherwise\n                   @nodiscard\n\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "doc/tags",
    "content": "detour\tdetour.txt\t/*detour*\ndetour-algorithm\tdetour.txt\t/*detour-algorithm*\ndetour-features\tdetour.txt\t/*detour-features*\ndetour-into\tdetour.txt\t/*detour-into*\ndetour-movements\tdetour.txt\t/*detour-movements*\ndetour.Detour\tdetour.txt\t/*detour.Detour*\ndetour.DetourCurrentWindow\tdetour.txt\t/*detour.DetourCurrentWindow*\ndetour.algorithm\tdetour.txt\t/*detour.algorithm*\ndetour.features\tdetour.txt\t/*detour.features*\ndetour.movements\tdetour.txt\t/*detour.movements*\nfeatures.CloseCurrentStack\tdetour.txt\t/*features.CloseCurrentStack*\nfeatures.CloseOnLeave\tdetour.txt\t/*features.CloseOnLeave*\nfeatures.HideAllDetours\tdetour.txt\t/*features.HideAllDetours*\nfeatures.RevealAllDetours\tdetour.txt\t/*features.RevealAllDetours*\nfeatures.ShowPathInTitle\tdetour.txt\t/*features.ShowPathInTitle*\nfeatures.UncoverWindow\tdetour.txt\t/*features.UncoverWindow*\nfeatures.UncoverWindowWithMouse\tdetour.txt\t/*features.UncoverWindowWithMouse*\nmovements.DetourWinCmdH\tdetour.txt\t/*movements.DetourWinCmdH*\nmovements.DetourWinCmdJ\tdetour.txt\t/*movements.DetourWinCmdJ*\nmovements.DetourWinCmdK\tdetour.txt\t/*movements.DetourWinCmdK*\nmovements.DetourWinCmdL\tdetour.txt\t/*movements.DetourWinCmdL*\nmovements.DetourWinCmdW\tdetour.txt\t/*movements.DetourWinCmdW*\nmovements._safe_state_handler\tdetour.txt\t/*movements._safe_state_handler*\n"
  },
  {
    "path": "examples/help-float.md",
    "content": "# Display help files in a Detour window\n\n```lua\nvim.keymap.set(\"n\", \"<A-h>\", function()\n\tlocal popup_id = require(\"detour\").Detour()\n\tif popup_id then\n\t\trequire(\"telescope.builtin\").live_grep({\n\t\t\tcwd = vim.fs.joinpath(vim.env.VIMRUNTIME, \"doc\"),\n\t\t})\n\telse\n\t\tlocal keys = vim.api.nvim_replace_termcodes(\":h \", true, true, true)\n\t\tvim.api.nvim_feedkeys(keys, \"n\", true)\n\tend\nend)\n```\n"
  },
  {
    "path": "examples/telescope.md",
    "content": "# Telescope keymaps\nHere are examples of useful keymaps where you use detour popups together with the [telescope plugin](https://github.com/nvim-telescope/telescope.nvim).\n\n### Terminal selection\nSelect an existing terminal to open in a popup. If none exist, open a new one. \n```lua\nvim.keymap.set('n', '<leader>t', function()\n    local terminal_buffer_found = false\n    -- Check if we there are any existing terminal buffers.\n    for _, buf in ipairs(vim.api.nvim_list_bufs()) do -- iterate through all buffers\n        if vim.api.nvim_buf_is_loaded(buf) then       -- only check loaded buffers\n            if vim.api.nvim_buf_get_option(buf, \"buftype\") == \"terminal\" then\n                terminal_buffer_found = true\n            end\n        end\n    end\n\n    require('detour').Detour()                      -- Open a detour popup\n    if terminal_buffer_found then\n        require('telescope.builtin').buffers({})    -- Open telescope prompt\n        vim.api.nvim_feedkeys(\"term://\", \"n\", true) -- populate prompt with \"term://\"\n    else\n        -- [OPTIONAL] Set the new window's current working directory to the directory of current file.\n        -- You can remove this line if you would prefer to open terminals from the\n        -- existing working directory.\n        vim.cmd.lcd(vim.fn.expand(\"%:p:h\"))\n        -- Since there are no existing terminal buffers, open a new one.\n        vim.cmd.terminal()\n        vim.cmd.startinsert()\n    end\nend)\n```\n"
  },
  {
    "path": "lua/detour/algorithm_doc.lua",
    "content": "---@mod detour.algorithm\n---@tag detour-algorithm\n---@brief [[\n---Detour's windowing algorithm computes the largest rectangle where a\n---floating window (detour) can be placed without overlapping windows that must\n---remain visible.\n---\n---Overview\n---\n--- Each detour has a list of \"reserved\" windows that it is allowed to cover.\n---\n--- When creating or resizing a detour, the algorithm will find the largest\n--- rectangular area where a floating window can go that covers only reserved\n--- windows. That rectangle will be the position and dimensions of the detour.\n---\n--- `Detour()` creates a detour `d` where all windows that are not currently\n--- reserved by an existing detour are reserved by `d`.\n---\n--- `DetourCurrentWindow()` creates a detour `d` where only the current window\n--- is reserved by `d`.\n---\n---Resizing\n---\n--- Whenever windows open, close, or get resized, detours will recalculate the\n--- largest area they can fill and dynamically reshape themselves. This allows\n--- them to make room for new windows or to expand to take space that has been\n--- freed up.\n---@brief ]]\nlocal algorithm_doc = {}\n\n--- Internal anchor to ensure module docs render. Do not use.\n---@private\nfunction algorithm_doc._doc_anchor() end\n\nreturn algorithm_doc\n"
  },
  {
    "path": "lua/detour/config.lua",
    "content": "---@mod detour.config\n---User configuration and defaults for detour.nvim.\n\n---@class detour.config.Options\n---@field title \"none\"|\"path\"\n\nlocal config = {}\n\n---@type detour.config.Options\nlocal defaults = {\n\ttitle = \"path\",\n}\n\n---@type detour.config.Options\nconfig.options = {}\n\n---Setup detour.nvim configuration.\n---@param args? detour.config.Options\nconfig.setup = function(args)\n\targs = args or {}\n\n\tlocal new_options =\n\t\tvim.tbl_deep_extend(\"force\", defaults, config.options, args or {})\n\n\tif not vim.tbl_contains({ \"none\", \"path\" }, new_options.title) then\n\t\tvim.api.nvim_echo({\n\t\t\t{\n\t\t\t\t'\"'\n\t\t\t\t\t.. tostring(new_options.title)\n\t\t\t\t\t.. '\" is an invalid value for title. Not changing detour configs.',\n\t\t\t},\n\t\t}, true, { err = true })\n\t\treturn\n\tend\n\n\tconfig.options = new_options\nend\n\nconfig.setup()\n\nreturn config\n"
  },
  {
    "path": "lua/detour/features.lua",
    "content": "---@mod detour.features\n---@brief [[\n--- Optional detour.nvim features.\n---@brief ]]\n---@tag detour-features\n\nlocal features = {}\n\nlocal util = require(\"detour.util\")\nlocal internal = require(\"detour.internal\")\n\n---Update the detour window title to the current buffer path relative to cwd.\n---@param window_id integer\n---@return nil\nlocal function update_title(window_id)\n\tlocal tabwin = vim.fn.win_id2tabwin(window_id)\n\tlocal tabnr, winnr = unpack(tabwin)\n\tif tabnr == 0 and winnr == 0 then\n\t\treturn\n\tend\n\tlocal buffer_id = vim.api.nvim_win_get_buf(window_id)\n\tlocal path = vim.api.nvim_buf_get_name(buffer_id)\n\tlocal home = vim.fn.getcwd(winnr, tabnr)\n\tlocal title = vim.fn.fnamemodify(path, \":.\")\n\tif title:sub(1, #home) == home then\n\t\ttitle = title:sub(#home + 1)\n\tend\n\tvim.api.nvim_win_set_config(\n\t\twindow_id,\n\t\tvim.tbl_extend(\n\t\t\t\"force\",\n\t\t\tvim.api.nvim_win_get_config(window_id),\n\t\t\t{ title = title }\n\t\t)\n\t)\nend\n\n---Show the buffer path in the given popup's title and keep it updated.\n---@param popup_id integer\n---@return nil\nfunction features.ShowPathInTitle(popup_id)\n\trequire(\"detour.show_path_in_title\")\n\n\tif\n\t\tnext(vim.api.nvim_get_autocmds({\n\t\t\tpattern = \"DetourUpdateTitle\" .. util.stringify(popup_id),\n\t\t\tgroup = internal.construct_augroup_name(popup_id),\n\t\t})) ~= nil\n\tthen\n\t\t-- ShowPathInTitle already called for this popup.\n\t\treturn\n\tend\n\n\tupdate_title(popup_id)\n\n\tvim.api.nvim_create_autocmd({ \"User\" }, {\n\t\tpattern = \"DetourUpdateTitle\" .. util.stringify(popup_id),\n\t\tgroup = internal.construct_augroup_name(popup_id),\n\t\tcallback = function()\n\t\t\tif not util.is_open(popup_id) then\n\t\t\t\treturn true\n\t\t\tend\n\t\t\tupdate_title(popup_id)\n\t\tend,\n\t})\nend\n\n---Close the popup when focus leaves to a non-floating window.\n---@param popup_id integer\n---@return nil\nfunction features.CloseOnLeave(popup_id)\n\t-- This autocmd will close the created detour popup when you focus on a different window.\n\tvim.api.nvim_create_autocmd({ \"WinEnter\" }, {\n\t\tgroup = internal.construct_augroup_name(popup_id),\n\t\tcallback = function()\n\t\t\tlocal curr_window = vim.api.nvim_get_current_win()\n\t\t\t-- Skip cases where we are entering popups (eg, menus, nested popups, the detour popup itself).\n\t\t\tif vim.api.nvim_win_get_config(curr_window).relative ~= \"\" then\n\t\t\t\treturn\n\t\t\tend\n\n\t\t\t-- Check to make sure the popup has not already been closed\n\t\t\tif util.is_open(popup_id) then\n\t\t\t\tvim.api.nvim_win_close(popup_id, false)\n\t\t\tend\n\t\tend,\n\t\tnested = true,\n\t})\nend\n\n---Prevent detours from covering the provided window.\n---@param window integer\n---@return boolean\nfunction features.UncoverWindow(window)\n\tlocal ok = internal.unreserve_window(window)\n\tif ok then\n\t\tvim.api.nvim_exec_autocmds(\"VimResized\", {})\n\tend\n\treturn ok\nend\n\n---Prompt to click a window and mark it as uncovered by detours.\n---@return nil\nfunction features.UncoverWindowWithMouse()\n\tlocal prev_mouse = vim.o.mouse\n\tif not prev_mouse:match(\"a\") then\n\t\tvim.o.mouse = \"a\"\n\tend\n\tvim.api.nvim_echo(\n\t\t{ { \"Click a window (Press any key to cancel)…\", \"Question\" } },\n\t\tfalse,\n\t\t{}\n\t)\n\n\tfeatures.HideAllDetours()\n\tvim.g.detour_temp_uncover = 0\n\tvim.cmd([[\n        let c = getchar()\n        if c == \"\\<LeftMouse>\" && v:mouse_win > 0\n            let g:detour_temp_uncover=1\n        endif\n    ]])\n\n\tfeatures.RevealAllDetours()\n\n\tvim.o.mouse = prev_mouse\n\tvim.cmd(\"echo '' | redraw!\") -- clear the prompt\n\n\tif vim.g.detour_temp_uncover == 0 then\n\t\tvim.o.mouse = prev_mouse\n\t\treturn\n\tend\n\n\tlocal m = vim.fn.getmousepos()\n\tlocal winid = util.base_at_screenpos(\n\t\tvim.api.nvim_get_current_tabpage(),\n\t\tm.screenrow,\n\t\tm.screencol\n\t)\n\n\tif winid then\n\t\tfeatures.UncoverWindow(winid)\n\tend\nend\n\n---Temporarily hide all detours in current tabpage.\n---@return nil\nfunction features.HideAllDetours()\n\tfor _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do\n\t\tif internal.is_detour(win) then\n\t\t\tvim.api.nvim_win_set_config(win, { hide = true })\n\t\tend\n\tend\n\tvim.cmd(\"redraw!\")\nend\n\n---Reveal all detours previously hidden in current tabpage.\n---@return nil\nfunction features.RevealAllDetours()\n\tfor _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do\n\t\tif internal.is_detour(win) then\n\t\t\tvim.api.nvim_win_set_config(win, { hide = false })\n\t\tend\n\tend\n\tvim.cmd(\"redraw!\")\nend\n\n--- Closes current detour along with all detours that it covers and covers it.\n--- When not inside a detour, this function is a no-op.\n---@return boolean success true if close operation succeeded and false otherwise\n---@nodiscard\nfunction features.CloseCurrentStack()\n\tinternal.garbage_collect()\n\n\tlocal current_window = vim.api.nvim_get_current_win()\n\tlocal covered = internal.get_reserved_windows(current_window)\n\twhile covered do\n\t\tlocal parent = covered[1]\n\t\tvim.api.nvim_win_close(current_window, false)\n\t\tif vim.api.nvim_get_current_win() == current_window then\n\t\t\t-- Close operation failed\n\t\t\treturn false\n\t\tend\n\t\tvim.fn.win_gotoid(parent)\n\t\tcurrent_window = parent\n\t\tcovered = internal.get_reserved_windows(vim.api.nvim_get_current_win())\n\tend\n\n\treturn true\nend\n\nreturn features\n"
  },
  {
    "path": "lua/detour/init.lua",
    "content": "---@mod detour\n---@tag detour-into\n---@brief [[\n---Neovim/Vim's floating windows are a great utility to use in plugins and\n---functions, but they cannot be used manually as splits are. This is because\n---creating floats is not simple like calling `:split` or `:vsplit`.\n---`vim.api.nvim_open_win(...)` requires coordinates and dimensions to make\n---a float which is too tedious to do by hand.\n---\n---Detour.nvim brings a single new feature to Neovim: detour windows (aka\n---detours). Detours are floating windows with the ease-of-use of splits.\n---\n---They dynamically shape themselves to cover as much of a given area as possible.\n---They can cover:\n---* the whole screen (`require(\"detour\").Detour()`)\n---* the current window (`require(\"detour\").DetourCurrentWindow()`)\n---* the current detour (both of the above functions would work)\n---\n---Detours will make sure not to overlap each other unless when a detour is\n---nested within another.\n---\n---You will find that there are many cases where using a large floating window is\n---preferable to creating a smaller split window. On top of that, the nesting\n---behavior of detours allows you to take a long \"detour\" into other files/locations\n---without losing your place in your regular windows. Take a detour, look at\n---other locations, close the detour, and find your original windows as they\n---were when you left.\n---@brief ]]\n\nlocal detour = {}\n\nlocal util = require(\"detour.util\")\nlocal internal = require(\"detour.internal\")\nlocal algo = require(\"detour.windowing_algorithm\")\nlocal settings = require(\"detour.config\")\n\n---Resize an existing popup and update covered windows' focusability.\n---@param window_id integer\n---@param new_window_opts table\nlocal function resize_popup(window_id, new_window_opts)\n\tlocal current_window_opts = vim.api.nvim_win_get_config(window_id)\n\tvim.api.nvim_win_set_config(\n\t\twindow_id,\n\t\tvim.tbl_extend(\"force\", current_window_opts, new_window_opts)\n\t)\n\n\t-- Not sure why this loop is necessary.\n\tfor _, covered_window in\n\t\tipairs(internal.get_reserved_windows(window_id) or {})\n\tdo\n\t\tif util.is_floating(covered_window) then\n\t\t\tvim.api.nvim_win_set_config(\n\t\t\t\tcovered_window,\n\t\t\t\tvim.tbl_extend(\n\t\t\t\t\t\"force\",\n\t\t\t\t\tvim.api.nvim_win_get_config(covered_window),\n\t\t\t\t\t{ focusable = not util.overlap(covered_window, window_id) }\n\t\t\t\t)\n\t\t\t)\n\t\tend\n\tend\n\n\t-- Sometimes, resizing terminal buffers can end up scrolling the terminal UI\n\t-- horizontally so that you only see a portion of the terminal UI. This code\n\t-- scrolls the terminal UI so that the window keeps showing the leftmost\n\t-- column\n\t--\n\t-- Condition on terminal mode (instead of checking whether buffer is\n\t-- terminal-type) just in case user was intentionally looking at a specific\n\t-- location in visual or normal mode.\n\tif vim.fn.mode() == \"t\" then\n\t\tlocal row = vim.api.nvim_win_get_position(window_id)[1]\n\t\tvim.api.nvim_win_set_cursor(window_id, { row, 0 })\n\tend\n\n\t-- Fully complete resizing before propogating event.\n\tvim.api.nvim_exec_autocmds(\"User\", {\n\t\tpattern = \"DetourPopupResized\" .. util.stringify(window_id),\n\t})\nend\n\n---Create a nested popup above the current floating window.\n---@return integer|nil popup_id\nlocal function popup_above_float()\n\tlocal parent = vim.api.nvim_get_current_win()\n\n\tif vim.tbl_contains(internal.list_reserved_windows(), parent) then\n\t\tvim.api.nvim_echo({\n\t\t\t{\n\t\t\t\t\"[detour.nvim] This popup already has a child nested inside it: \"\n\t\t\t\t\t.. parent,\n\t\t\t},\n\t\t}, true, { err = true })\n\t\treturn nil\n\tend\n\n\tlocal parent_zindex = util.get_maybe_zindex(parent) or 0\n\tlocal window_opts = algo.construct_nest(parent, parent_zindex + 1)\n\n\tlocal child =\n\t\tvim.api.nvim_open_win(vim.api.nvim_win_get_buf(0), true, window_opts)\n\tif not internal.record_popup(child, { parent }) then\n\t\tvim.api.nvim_win_close(child, true)\n\t\treturn nil\n\tend\n\n\tlocal augroup_id =\n\t\tvim.api.nvim_create_augroup(internal.construct_augroup_name(child), {})\n\tvim.api.nvim_create_autocmd({ \"User\" }, {\n\t\tpattern = \"DetourPopupResized\" .. util.stringify(parent),\n\t\tgroup = augroup_id,\n\t\tcallback = function()\n\t\t\tif not util.is_open(child) then\n\t\t\t\tinternal.teardown_detour(child)\n\t\t\t\treturn\n\t\t\tend\n\n\t\t\tif util.is_open(parent) then\n\t\t\t\tresize_popup(child, algo.construct_nest(parent))\n\t\t\tend\n\t\tend,\n\t})\n\tvim.api.nvim_create_autocmd({ \"WinClosed\" }, {\n\t\tgroup = augroup_id,\n\t\tpattern = tostring(child),\n\t\tcallback = function()\n\t\t\tinternal.teardown_detour(child)\n\t\t\tif util.is_open(parent) then\n\t\t\t\tvim.fn.win_gotoid(parent)\n\t\t\tend\n\t\tend,\n\t\tnested = true, -- Trigger all the autocmds for entering a new window\n\t})\n\n\tif settings.options.title == \"path\" then\n\t\trequire(\"detour.features\").ShowPathInTitle(child)\n\tend\n\n\t-- We're running this to make sure initializing popups runs the same code\n\t-- path as updating popups\n\t-- We make sure to do this after all state and autocmds are set.\n\tvim.api.nvim_exec_autocmds(\"User\", {\n\t\tpattern = \"DetourPopupResized\" .. util.stringify(parent),\n\t})\n\treturn child\nend\n\n---Create a base popup covering the given windows (or all non-floating windows).\n---@param bufnr integer\n---@param reserve_windows integer[]?\n---@return integer|nil popup_id\nlocal function popup(bufnr, reserve_windows)\n\tlocal tab_id = vim.api.nvim_get_current_tabpage()\n\treserve_windows = reserve_windows\n\t\tor vim.tbl_filter(function(window)\n\t\t\treturn not (\n\t\t\t\tutil.is_floating(window)\n\t\t\t\tor vim.tbl_contains(internal.list_reserved_windows(), window)\n\t\t\t)\n\t\tend, vim.api.nvim_tabpage_list_wins(tab_id))\n\n\tif #reserve_windows == 0 then\n\t\tvim.api.nvim_echo(\n\t\t\t{ { \"[detour.nvim] No windows provided in coverable_windows.\" } },\n\t\t\ttrue,\n\t\t\t{ err = true }\n\t\t)\n\t\treturn nil\n\tend\n\n\tfor _, window in ipairs(reserve_windows) do\n\t\tif util.is_floating(window) then\n\t\t\tvim.api.nvim_echo({\n\t\t\t\t{\n\t\t\t\t\t\"[detour.nvim] No floating windows allowed in base (ie, non-nested) popup \"\n\t\t\t\t\t\t.. window,\n\t\t\t\t},\n\t\t\t}, true, { err = true })\n\t\t\treturn nil\n\t\tend\n\n\t\tif vim.tbl_contains(internal.list_reserved_windows(), window) then\n\t\t\tvim.api.nvim_echo({\n\t\t\t\t{\n\t\t\t\t\t\"[detour.nvim] This window is already reserved by another detour: \"\n\t\t\t\t\t\t.. window,\n\t\t\t\t},\n\t\t\t}, true, { err = true })\n\t\t\treturn nil\n\t\tend\n\tend\n\n\tlocal window_opts = algo.construct_window_opts(reserve_windows, tab_id)\n\tif window_opts == nil then\n\t\treturn nil\n\tend\n\n\tlocal popup_id = vim.api.nvim_open_win(bufnr, true, window_opts)\n\tif not internal.record_popup(popup_id, reserve_windows) then\n\t\tvim.api.nvim_win_close(popup_id, true)\n\t\treturn nil\n\tend\n\n\tlocal augroup_id = vim.api.nvim_create_augroup(\n\t\tinternal.construct_augroup_name(popup_id),\n\t\t{}\n\t)\n\n\tvim.api.nvim_create_autocmd({ \"WinResized\" }, {\n\t\tgroup = augroup_id,\n\t\tcallback = function(event)\n\t\t\tlocal reserved = internal.get_reserved_windows(popup_id)\n\t\t\tif reserved == nil then\n\t\t\t\tinternal.teardown_detour(popup_id)\n\t\t\t\treturn\n\t\t\tend\n\t\t\t-- WinResized populates vim.v.event.windows but VimResized does not\n\t\t\t-- so we default to listing all windows.\n\t\t\t-- Use event.data.windows for tests.\n\t\t\tlocal windows = vim.tbl_get(vim.v[\"event\"], \"windows\") == nil\n\t\t\t\t\tand event.data.windows\n\t\t\t\tor vim.v[\"event\"][\"windows\"]\n\t\t\tlocal changed_window =\n\t\t\t\tassert(windows[1], \"no windows listed in WinResized event\")\n\t\t\tlocal changed_tab = vim.api.nvim_win_get_tabpage(changed_window)\n\t\t\tif tab_id == changed_tab then\n\t\t\t\tlocal new_window_opts =\n\t\t\t\t\talgo.construct_window_opts(reserved, tab_id)\n\t\t\t\tif new_window_opts then\n\t\t\t\t\tresize_popup(popup_id, new_window_opts)\n\t\t\t\tend\n\t\t\tend\n\t\tend,\n\t})\n\n\tvim.api.nvim_create_autocmd({ \"VimResized\" }, {\n\t\tgroup = augroup_id,\n\t\tcallback = function()\n\t\t\tinternal.garbage_collect()\n\t\t\tlocal reserved = internal.get_reserved_windows(popup_id)\n\t\t\tif reserved == nil then\n\t\t\t\tinternal.teardown_detour(popup_id)\n\t\t\t\treturn\n\t\t\tend\n\n\t\t\tlocal new_window_opts = algo.construct_window_opts(reserved, tab_id)\n\t\t\t-- If there is an issue that prevents a valid configuration for the\n\t\t\t-- detour, just leave it for the user to manually clean up.\n\t\t\tif new_window_opts then\n\t\t\t\tresize_popup(popup_id, new_window_opts)\n\t\t\tend\n\t\tend,\n\t})\n\n\tvim.api.nvim_create_autocmd({ \"WinClosed\" }, {\n\t\tgroup = augroup_id,\n\t\tpattern = \"\" .. popup_id,\n\t\tcallback = function()\n\t\t\tlocal reserved = internal.get_reserved_windows(popup_id)\n\t\t\tinternal.teardown_detour(popup_id)\n\t\t\tfor _, base in ipairs(reserved) do\n\t\t\t\tif\n\t\t\t\t\tvim.tbl_contains(\n\t\t\t\t\t\tvim.api.nvim_tabpage_list_wins(tab_id),\n\t\t\t\t\t\tbase\n\t\t\t\t\t)\n\t\t\t\tthen\n\t\t\t\t\tvim.fn.win_gotoid(base)\n\t\t\t\t\treturn\n\t\t\t\tend\n\t\t\tend\n\t\tend,\n\t\tnested = true, -- Trigger all the autocmds for entering a new window\n\t})\n\n\tif settings.options.title == \"path\" then\n\t\trequire(\"detour.features\").ShowPathInTitle(popup_id)\n\tend\n\n\t-- We're running this to make sure initializing popups runs the same code\n\t-- path as updating popups. We make sure to do this after all state and\n\t-- autocmds are set.\n\tvim.api.nvim_exec_autocmds(\"VimResized\", {})\n\n\treturn popup_id\nend\n\n---Open a new detour window\n---\n---* If this is called from a non-detour window, the largest possible detour\n---window will be opened that does not overlap with any other detours.\n---* If this is called from a detour window, a detour will be opened nested\n---within just the current detour.\n---\n---There are cases where there is no space for a new detour window and\n---this function call will do nothing.\n---@return integer|nil popup_id returns detour's window id if successfully\n---created, nil otherwise\n---@nodiscard\n---@usage `\n--- local window_id = require(\"detour\").Detour()\n--- if not window_id then\n---     -- New detour could not be made so stop execution\n---     return\n--- end\n---\n--- -- New detour window is open and cursor is moved to it.`\ndetour.Detour = function()\n\tinternal.garbage_collect()\n\tif util.is_floating(vim.api.nvim_get_current_win()) then\n\t\treturn popup_above_float()\n\tend\n\n\treturn popup(vim.api.nvim_get_current_buf())\nend\n\n---Open a detour popup covering only the current window.\n---\n---There are cases where there is no space for a new detour window and\n---this function call will do nothing.\n---@return integer|nil popup_id returns detour's window id if successfully\n---created, nil otherwise\n---@nodiscard\n---@usage `\n--- local window_id = require(\"detour\").DetourCurrentWindow()\n--- if not window_id then\n--- \t-- New detour could not be made so stop execution\n--- \treturn\n--- end\n---\n--- -- New detour window is open and cursor is moved to it.`\ndetour.DetourCurrentWindow = function()\n\tinternal.garbage_collect()\n\n\tif util.is_floating(vim.api.nvim_get_current_win()) then\n\t\treturn popup_above_float()\n\tend\n\n\treturn popup(\n\t\tvim.api.nvim_get_current_buf(),\n\t\t{ vim.api.nvim_get_current_win() }\n\t)\nend\n\ndetour.setup = require(\"detour.config\").setup\n\nreturn detour\n"
  },
  {
    "path": "lua/detour/internal.lua",
    "content": "-- DO NOT DEPEND ON THIS FILE!\n-- This is an \"internal\" file and can have breaking changes without warning.\n\n---@mod detour.internal\n---Internal implementation details. Not part of the public API.\n\nlocal internal = {}\n\n---@class detour.internal\n---@field construct_augroup_name fun(window_id: integer): string\n---@field teardown_detour fun(window_id: integer)\n---@field record_popup fun(popup_id: integer, coverable_windows: integer[]): boolean\n---@field list_popups fun(): integer[]\n---@field list_reserved_windows fun(): integer[]\n---@field garbage_collect fun()\n---@field is_detour fun(window: integer): boolean\n---@field get_reserved_windows fun(popup_id: integer): integer[]|nil\n---@field unreserve_window fun(window: integer): boolean\n\n---@type table<integer, integer[]>\nlocal popup_to_reserved_windows = {}\n\n---@param window_id integer\n---@return string\nfunction internal.construct_augroup_name(window_id)\n\treturn \"detour-\" .. window_id\nend\n\n-- Needs to be idempotent\n---@param window_id integer\nfunction internal.teardown_detour(window_id)\n\t-- Be tolerant if the augroup was already removed by another path.\n\tpcall(\n\t\tvim.api.nvim_del_augroup_by_name,\n\t\tinternal.construct_augroup_name(window_id)\n\t)\n\tfor _, covered_window in\n\t\tipairs(internal.get_reserved_windows(window_id) or {})\n\tdo\n\t\tif vim.api.nvim_win_get_config(covered_window).relative ~= \"\" then\n\t\t\tvim.api.nvim_win_set_config(\n\t\t\t\tcovered_window,\n\t\t\t\tvim.tbl_extend(\n\t\t\t\t\t\"force\",\n\t\t\t\t\tvim.api.nvim_win_get_config(covered_window),\n\t\t\t\t\t{ focusable = true }\n\t\t\t\t)\n\t\t\t)\n\t\tend\n\tend\n\tpopup_to_reserved_windows[window_id] = nil\nend\n\nfunction internal.is_detour(window)\n\treturn popup_to_reserved_windows[window] ~= nil\nend\n\n---@param popup_id integer\n---@return integer[]|nil\nfunction internal.get_reserved_windows(popup_id)\n\tif popup_to_reserved_windows[popup_id] == nil then\n\t\treturn nil\n\tend\n\n\t-- Clean up any windows that have already been closed\n\tpopup_to_reserved_windows[popup_id] = vim.tbl_filter(function(window_id)\n\t\treturn vim.tbl_contains(vim.api.nvim_list_wins(), window_id)\n\tend, popup_to_reserved_windows[popup_id])\n\n\treturn popup_to_reserved_windows[popup_id]\nend\n\n---@param popup_id integer\n---@param coverable_windows integer[]\n---@return boolean\nfunction internal.record_popup(popup_id, coverable_windows)\n\tlocal open_windows = vim.api.nvim_list_wins()\n\tcoverable_windows = vim.tbl_filter(function(window_id)\n\t\treturn vim.tbl_contains(open_windows, window_id)\n\tend, coverable_windows)\n\n\tif #coverable_windows == 0 then\n\t\tvim.api.nvim_echo({\n\t\t\t{\n\t\t\t\t\"[detour.nvim] You must provide at least one valid (open) coverable window.\",\n\t\t\t},\n\t\t}, true, { err = true })\n\t\treturn false\n\tend\n\tpopup_to_reserved_windows[popup_id] = coverable_windows\n\treturn true\nend\n\n---@return integer[]\nfunction internal.list_popups()\n\treturn vim.tbl_keys(popup_to_reserved_windows)\nend\n\n---@return integer[]\nfunction internal.list_reserved_windows()\n\tlocal windows = vim.api.nvim_list_wins()\n\treturn vim.iter(vim.tbl_values(popup_to_reserved_windows))\n\t\t:flatten()\n\t\t:filter(function(w)\n\t\t\treturn vim.tbl_contains(windows, w) -- make sure window is still open\n\t\tend)\n\t\t:totable()\nend\n\n---@param window integer\n---@return boolean\nfunction internal.unreserve_window(window)\n\tinternal.garbage_collect()\n\tlocal changed = false\n\tlocal copy = vim.tbl_extend(\"force\", popup_to_reserved_windows, {})\n\tfor popup, reserved_windows in pairs(popup_to_reserved_windows) do\n\t\tcopy[popup] = vim.iter(reserved_windows)\n\t\t\t:filter(function(reserved)\n\t\t\t\tif reserved ~= window then\n\t\t\t\t\treturn true\n\t\t\t\tend\n\t\t\t\tchanged = true\n\t\t\t\treturn false\n\t\t\tend)\n\t\t\t:totable()\n\t\tif #copy[popup] == 0 then\n\t\t\tvim.api.nvim_echo({\n\t\t\t\t{\n\t\t\t\t\t\"[detour.nvim] A detour must have at least one window to float over. Detour id: \"\n\t\t\t\t\t\t.. popup,\n\t\t\t\t},\n\t\t\t}, true, { err = true })\n\t\t\treturn false\n\t\tend\n\tend\n\tpopup_to_reserved_windows = copy\n\treturn changed\nend\n\n-- Neovim autocmd events are quite nuanced:\n-- 1. Autocmds do not trigger autocmd events by default (you need to set `nested\n--    = true` to do that).\n-- 2. WinClosed autocmds do not trigger WinClosed events even if `nested = true`.\n-- 3. Even with `nested = true`, there is a limit to how many nested events\n--    Neovim will trigger (max depth is 10).\n-- Hence, there are possible cases where popup detours will be closed by the\n-- user's autocmds without triggering a WinClosed event. To address this, we\n-- must make sure to update the plugin's state before executing each user\n-- command. Also, we must double check what windows are still open during this\n-- plugin's autocmd callbacks.\nfunction internal.garbage_collect()\n\tfor _, popup_id in ipairs(internal.list_popups()) do\n\t\tif not vim.tbl_contains(vim.api.nvim_list_wins(), popup_id) then\n\t\t\tinternal.teardown_detour(popup_id)\n\t\tend\n\tend\nend\n\nassert(\n\tvim.fn.timer_start(\n\t\t300,\n\t\tvim.schedule_wrap(internal.garbage_collect),\n\t\t{ [\"repeat\"] = -1 }\n\t) ~= -1,\n\t\"[detour.nvim] Failed to create garbage_collect timer.\"\n)\n\nlocal group = vim.api.nvim_create_augroup(\"detour_internal\", {})\n\nlocal just_entered_window = false\nvim.api.nvim_create_autocmd({ \"WinEnter\" }, {\n\tgroup = group,\n\tcallback = function()\n\t\tjust_entered_window = true\n\tend,\n})\n\n-- If the user interacts with a window, we should prevent detours from covering\n-- it.\nvim.api.nvim_create_autocmd({ \"CursorMoved\", \"ModeChanged\" }, {\n\tgroup = group,\n\tcallback = function()\n\t\t-- Ignore this event if `WinEnter` just happened.\n\t\tif just_entered_window == true then\n\t\t\tjust_entered_window = false\n\t\t\treturn\n\t\tend\n\n\t\tif internal.unreserve_window(vim.api.nvim_get_current_win()) then\n\t\t\tvim.api.nvim_exec_autocmds(\"VimResized\", {})\n\t\tend\n\tend,\n})\n\nreturn internal\n"
  },
  {
    "path": "lua/detour/movements.lua",
    "content": "---@mod detour.movements\n---@brief [[\n---Detour.nvim breaks window navigation commands such as\n---`<C-w>w`, `<C-w>j`, or `vim.cmd.wincmd(\"h\")`. Instead of\n---using those, the user MUST use the following window navigation\n---commands that this plugin provides that implements moving\n---between windows while skipping over windows covered by detours.\n---\n---NOTE: Regular window movements such as `<C-w>w`, `<C-w>j`,\n---`vim.cmd.wincmd(\"h\")` should still work in automated\n---scripts/functions. Still, you may find it more useful to use detour's\n---\"detour-aware\" movement functions in your scripts/functions as well.\n---@brief ]]\n\n---@tag detour-movements\n\nlocal movements = {}\n\nlocal util = require(\"detour.util\")\nlocal internal = require(\"detour.internal\")\n\nlocal module_augroup = \"detour-movements\"\nvim.api.nvim_create_augroup(module_augroup, { clear = true })\n\n---DO NOT USE. FOR TESTING ONLY.\nmovements._safe_state_handler = function()\n\tinternal.garbage_collect()\n\tvim.fn.win_gotoid(util.find_top_popup())\nend\n\n-- Sometimes the cursor can end up behind a detour (eg, when a window is\n-- closed). In these cases just move the cursor to an appropriate place.\n-- Using SafeState here means this autocmd will not interfere with automated\n-- movements.\nvim.api.nvim_create_autocmd({ \"SafeState\" }, {\n\tgroup = module_augroup,\n\tcallback = movements._safe_state_handler,\n\tnested = true,\n})\n\n--- Switch to a window to the right. Skip over any non-floating windows\n--- covered by a detour.\n---@return nil\n---@usage `\n--- local detour_moves = require(\"detour.movements\")\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-l>\", detour_moves.DetourWinCmdL)\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-w>l\", detour_moves.DetourWinCmdL)\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-l>\", detour_moves.DetourWinCmdL)\n---`\nfunction movements.DetourWinCmdL()\n\tlocal covered_bases = util.find_covered_bases(\n\t\tvim.api.nvim_get_current_win()\n\t) or { vim.api.nvim_get_current_win() }\n\ttable.sort(covered_bases, function(windowA, windowB)\n\t\tlocal _, _, _, rightA = util.get_text_area_dimensions(windowA)\n\t\tlocal _, _, _, rightB = util.get_text_area_dimensions(windowB)\n\t\treturn rightA > rightB\n\tend)\n\tlocal base = covered_bases[1]\n\tfor _, window in ipairs(covered_bases) do\n\t\tlocal _, _, _, right = util.get_text_area_dimensions(window)\n\t\tif right ~= vim.o.columns then\n\t\t\tbase = window\n\t\t\tbreak\n\t\tend\n\tend\n\n\tvim.fn.win_gotoid(base)\n\tvim.cmd.wincmd(\"l\")\n\t-- It's possible to rely on the SafeState autocmd instead of explicitly\n\t-- moving to the top popup, but an explicit call allows this function to work\n\t-- properly when used in other code.\n\tvim.fn.win_gotoid(util.find_top_popup())\nend\n\n--- Switch to a window to the left. Skip over any non-floating windows\n--- covered by a detour.\n---@return nil\n---@usage `\n--- local detour_moves = require(\"detour.movements\")\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-h>\", detour_moves.DetourWinCmdH)\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-w>h\", detour_moves.DetourWinCmdH)\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-h>\", detour_moves.DetourWinCmdH)\n---`\nfunction movements.DetourWinCmdH()\n\tlocal covered_bases = util.find_covered_bases(\n\t\tvim.api.nvim_get_current_win()\n\t) or { vim.api.nvim_get_current_win() }\n\ttable.sort(covered_bases, function(windowA, windowB)\n\t\tlocal _, _, leftA, _ = util.get_text_area_dimensions(windowA)\n\t\tlocal _, _, leftB, _ = util.get_text_area_dimensions(windowB)\n\t\treturn leftA < leftB\n\tend)\n\tlocal base = covered_bases[1]\n\tfor _, window in ipairs(covered_bases) do\n\t\tlocal _, _, left, _ = util.get_text_area_dimensions(window)\n\t\tif left ~= 0 then\n\t\t\tbase = window\n\t\t\tbreak\n\t\tend\n\tend\n\n\tvim.fn.win_gotoid(base)\n\tvim.cmd.wincmd(\"h\")\n\tvim.fn.win_gotoid(util.find_top_popup())\nend\n\n--- Switch to a window below. Skip over any non-floating windows\n--- covered by a detour.\n---@return nil\n---@usage `\n--- local detour_moves = require(\"detour.movements\")\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-j>\", detour_moves.DetourWinCmdJ)\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-w>j\", detour_moves.DetourWinCmdJ)\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-j>\", detour_moves.DetourWinCmdJ)\n---`\nfunction movements.DetourWinCmdJ()\n\tlocal covered_bases = util.find_covered_bases(\n\t\tvim.api.nvim_get_current_win()\n\t) or { vim.api.nvim_get_current_win() }\n\ttable.sort(covered_bases, function(windowA, windowB)\n\t\tlocal _, bottomA, _, _ = util.get_text_area_dimensions(windowA)\n\t\tlocal _, bottomB, _, _ = util.get_text_area_dimensions(windowB)\n\t\treturn bottomA > bottomB\n\tend)\n\tlocal base = covered_bases[1]\n\tfor _, window in ipairs(covered_bases) do\n\t\tlocal _, bottom, _, _ = util.get_text_area_dimensions(window)\n\t\tif\n\t\t\tbottom\n\t\t\t~= vim.o.lines\n\t\t\t\t- vim.o.cmdheight\n\t\t\t\t- (vim.o.laststatus == 0 and 0 or 1)\n\t\tthen -- subtract one for statusline\n\t\t\tbase = window\n\t\t\tbreak\n\t\tend\n\tend\n\n\tvim.fn.win_gotoid(base)\n\tvim.cmd.wincmd(\"j\")\n\tvim.fn.win_gotoid(util.find_top_popup())\nend\n\n--- Switch to a window above. Skip over any non-floating windows\n--- covered by a detour.\n---@return nil\n---@usage `\n--- local detour_moves = require(\"detour.movements\")\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-k>\", detour_moves.DetourWinCmdK)\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-w>k\", detour_moves.DetourWinCmdK)\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-k>\", detour_moves.DetourWinCmdK)\n---`\nfunction movements.DetourWinCmdK()\n\tlocal covered_bases = util.find_covered_bases(\n\t\tvim.api.nvim_get_current_win()\n\t) or { vim.api.nvim_get_current_win() }\n\ttable.sort(covered_bases, function(windowA, windowB)\n\t\tlocal topA, _, _, _ = util.get_text_area_dimensions(windowA)\n\t\tlocal topB, _, _, _ = util.get_text_area_dimensions(windowB)\n\t\treturn topA < topB\n\tend)\n\tlocal base = covered_bases[1]\n\tfor _, window in ipairs(covered_bases) do\n\t\tlocal top, _, _, _ = util.get_text_area_dimensions(window)\n\t\tif top ~= 0 then\n\t\t\tbase = window\n\t\t\tbreak\n\t\tend\n\tend\n\n\tvim.fn.win_gotoid(base)\n\tvim.cmd.wincmd(\"k\")\n\tvim.fn.win_gotoid(util.find_top_popup())\nend\n\n--- Switch windows in a cycle. Skip over any non-floating windows covered\n--- by a detour.\n---@return nil\n---@usage `\n--- local detour_moves = require(\"detour.movements\")\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-w>\", detour_moves.DetourWinCmdW)\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-w>w\", detour_moves.DetourWinCmdW)\n--- vim.keymap.set({ \"n\", \"t\" }, \"<C-w><C-w>\", detour_moves.DetourWinCmdW)\n---`\nfunction movements.DetourWinCmdW()\n\t-- We do not just repeatedly do `vim.cmd.wincmd(\"w\")` until we hit a detour\n\t-- or uncovered window because doing so could form a cycle that does not\n\t-- include all windows.\n\tlocal windows = vim.api.nvim_tabpage_list_wins(0)\n\t-- TODO: if there are no (visible) detours, just call vim.cmd.wincmd(\"w\").\n\tlocal current_top = util.find_top_popup()\n\n\t-- Collect all the top detour or naked base windows.\n\tlocal tops = {}\n\tfor _, window in ipairs(windows) do\n\t\ttops[util.find_top_popup(window)] = true\n\tend\n\n\t-- Sort all the top windows. Filter out the windows ordered before the\n\t-- current top.\n\tlocal ordered_tops = {}\n\tfor top in vim.spairs(tops) do\n\t\tif top > current_top then\n\t\t\tordered_tops[#ordered_tops + 1] = top\n\t\tend\n\tend\n\n\t-- In tops is empty, add the first top so that we cycle back to the\n\t-- beginning of the list.\n\tfor top in vim.spairs(tops) do\n\t\tordered_tops[#ordered_tops + 1] = top\n\t\tbreak\n\tend\n\n\tvim.fn.win_gotoid(ordered_tops[1])\nend\n\nreturn movements\n"
  },
  {
    "path": "lua/detour/show_path_in_title.lua",
    "content": "---@mod detour.show_path_in_title\n---Internal helper for updating popup titles.\n\n-- Put this code in its own library to make sure it's only run once.\n\nlocal util = require(\"detour.util\")\nlocal internal = require(\"detour.internal\")\n\n---@type integer\nlocal ns = vim.api.nvim_create_namespace(\"detour.nvim-ns\")\n\n---@type uv.uv_timer_t\nlocal timer = assert(vim.uv.new_timer())\n\n-- This implements a trailing debouce where each call to the debounced function\n-- will start a timer and cancel any existing timers for that function. The\n-- function will eventually be called with the arguments from its most recent\n-- call.\n---@param ms integer\n---@param fn fun(...: any)\n---@return fun(...: any)\nlocal function debounce(ms, fn)\n\treturn function(...)\n\t\tlocal argv = { ... }\n\t\ttimer:stop()\n\t\ttimer:start(ms, 0, function()\n\t\t\tvim.schedule_wrap(fn)(unpack(argv))\n\t\tend)\n\tend\nend\n\n---@param _ any\n---@param window_id integer\nlocal function update_title(_, window_id)\n    if vim.tbl_contains(internal.list_popups(), window_id) then\n        vim.api.nvim_exec_autocmds(\"User\", {\n            pattern = \"DetourUpdateTitle\" .. util.stringify(window_id),\n        })\n    end\nend\n\n-- The reason why we're using a callback on decoration_provider instead of using an autocmd on BufEnter is because we\n-- want to trigger a title update while browsing through netrw directories and that doesn't trigger BufEnter.\n--\n-- From `api.txt`:\n-- (About nvim_set_decoration_provider) doing anything other than setting\n-- extmarks is considered experimental. Doing things like changing options are\n-- not explicitly forbidden, but is likely to have unexpected consequences (such\n-- as 100% CPU consumption). Doing `vim.rpcnotify` should be OK, but\n-- `vim.rpcrequest` is quite dubious for the moment.\n--\n-- I debounce `update_title` since rapidly changing the title with `on_win`\n-- causes neovim to freeze.\nvim.api.nvim_set_decoration_provider(ns, {\n\ton_win = debounce(50, update_title),\n})\nreturn {}\n"
  },
  {
    "path": "lua/detour/util.lua",
    "content": "---@mod detour.util\n---Utilities for window geometry, state checks, and helpers.\n\nlocal util = {}\n\n---@class detour.util\n---@field Set fun(list: any[]): table<any, boolean>\n---@field contains_element fun(array: any[], target: any): boolean\n---@field contains_key fun(array: table, target: any): boolean\n---@field contains_value fun(array: table, target: any): boolean\n---@field get_text_area_dimensions fun(window_id: integer): integer, integer, integer, integer\n---@field is_floating fun(window_id: integer): boolean\n---@field get_maybe_zindex fun(window_id: integer): integer|nil\n---@field overlap fun(window_a: integer, window_b: integer): boolean\n---@field find_top_popup fun(window?: integer): integer\n---@field find_covered_bases fun(window_id: integer): integer[]|nil\n---@field find_covered_windows fun(window_id: integer): integer[]\n---@field is_open fun(window_id: integer): boolean\n---@field stringify fun(number: integer): string\n---@field pairs_by_keys fun(t: table, f?: fun(a:any,b:any):boolean): fun(): any, any\n---@field is_statusline_global fun(): boolean\n---@field base_at_screenpos fun(tab_id: integer, screenrow: integer, screencol: integer): integer|nil\n\nlocal internal = require(\"detour.internal\")\n\n---@param list any[]\n---@return table<any, boolean>\nfunction util.Set(list)\n\tlocal set = {}\n\tfor _, l in ipairs(list) do\n\t\tset[l] = true\n\tend\n\treturn set\nend\n\n---@param array any[]\n---@param target any\n---@return boolean\nfunction util.contains_element(array, target)\n\tfor _, value in ipairs(array) do\n\t\tif value == target then\n\t\t\treturn true\n\t\tend\n\tend\n\treturn false\nend\n\n---@param array table\n---@param target any\n---@return boolean\nfunction util.contains_key(array, target)\n\tfor key, _ in pairs(array) do\n\t\tif key == target then\n\t\t\treturn true\n\t\tend\n\tend\n\treturn false\nend\n\n---@param array table\n---@param target any\n---@return boolean\nfunction util.contains_value(array, target)\n\tfor _, value in pairs(array) do\n\t\tif value == target then\n\t\t\treturn true\n\t\tend\n\tend\n\treturn false\nend\n\n--- Returns the positions of top, bottom, left, and right of a given window's text area.\n--- The statusline is not included in the text area. Bottom and right are exclusive.\n---@param window_id integer\n---@return integer top, integer bottom, integer left, integer right\nfunction util.get_text_area_dimensions(window_id)\n\tlocal top, left = unpack(vim.api.nvim_win_get_position(window_id))\n\tlocal bottom = top + vim.api.nvim_win_get_height(window_id)\n\tlocal right = left + vim.api.nvim_win_get_width(window_id)\n\treturn top, bottom, left, right\nend\n\n---@param window_id integer\n---@return boolean\nfunction util.is_floating(window_id)\n\treturn vim.api.nvim_win_get_config(window_id).relative ~= \"\"\nend\n\n--- Returns the zindex for the given window, if floating, otherwise nil.\n---@param window_id integer\n---@return integer|nil\nfunction util.get_maybe_zindex(window_id)\n\treturn vim.api.nvim_win_get_config(window_id).zindex\nend\n\n---@param positions_a { [1]: integer, [2]: integer, [3]: integer, [4]: integer }\n---@param positions_b { [1]: integer, [2]: integer, [3]: integer, [4]: integer }\n---@return boolean\nlocal function overlap_helper(positions_a, positions_b)\n\tlocal top_a, bottom_a, left_a, right_a = unpack(positions_a)\n\tlocal top_b, bottom_b, left_b, right_b = unpack(positions_b)\n\tif math.max(left_a, left_b) >= math.min(right_a, right_b) then\n\t\treturn false\n\tend\n\n\tif math.max(top_a, top_b) >= math.min(bottom_a, bottom_b) then\n\t\treturn false\n\tend\n\n\treturn true\nend\n\n---@param window_a integer\n---@param window_b integer\n---@return boolean\nfunction util.overlap(window_a, window_b)\n\treturn overlap_helper(\n\t\t{ util.get_text_area_dimensions(window_a) },\n\t\t{ util.get_text_area_dimensions(window_b) }\n\t)\nend\n\n---@param window? integer\n---@return integer window_id\nfunction util.find_top_popup(window)\n\tlocal window_id = window or vim.api.nvim_get_current_win()\n\tlocal all_coverable_windows = internal.list_reserved_windows()\n\tfor _, popup in ipairs(internal.list_popups()) do\n\t\tif\n\t\t\tnot vim.list_contains(all_coverable_windows, popup) -- ignore popups with popups nested in them\n\t\t\tand vim.tbl_contains(util.find_covered_windows(popup), window_id)\n\t\tthen\n\t\t\treturn popup\n\t\tend\n\tend\n\treturn window_id -- no popup that covers the current window was found\nend\n\n-- Finds all base windows that are covered by the provided popup.\n-- If the provided window is not a popup, returns the given argument.\n---@param window_id integer\n---@return integer[]|nil\nfunction util.find_covered_bases(window_id)\n\tassert(util.is_open(window_id), tostring(window_id) .. \" is not open\")\n\tlocal current_window = window_id\n\tlocal coverable_bases = nil\n\twhile internal.get_reserved_windows(current_window) do\n\t\tcoverable_bases = internal.get_reserved_windows(current_window) or {}\n\t\tassert(\n\t\t\t#coverable_bases > 0,\n\t\t\t\"[detour.nvim] There should never be an empty array in popup_to_covered_windows.\"\n\t\t)\n\t\t-- We iterate on only the first covered window because there are two cases:\n\t\t-- A: there is exactly one covered window and it's another detour.\n\t\t-- B: there is one or more covered windows and none of them are detours. We've found our covered base windows. Hence, this would be the last iteration of this loop.\n\t\tcurrent_window = coverable_bases[1]\n\tend\n\n\t-- This covers the case where the window_id is not a detour popup and we never enter the above loop.\n\tif coverable_bases == nil then\n\t\treturn nil\n\tend\n\n\treturn vim.tbl_filter(function(base)\n\t\treturn util.overlap(base, window_id)\n\tend, coverable_bases)\nend\n\n-- Finds all windows that are covered by the provided popup.\n-- If the provided window is not a popup, returns the given argument.\n---@param window integer\n---@return integer[]\nfunction util.find_covered_windows(window)\n\tlocal current_window = window\n\tlocal coverable_windows = {}\n\twhile internal.get_reserved_windows(current_window) do\n\t\tcoverable_windows[#coverable_windows + 1] =\n\t\t\tinternal.get_reserved_windows(current_window)\n\t\tassert(\n\t\t\t#coverable_windows[#coverable_windows] > 0,\n\t\t\t\"[detour.nvim] There should never be an empty array in popup_to_covered_windows.\"\n\t\t)\n\t\t-- We iterate on only the first covered window because there are two cases:\n\t\t-- A: there is exactly one covered window and it's another detour.\n\t\t-- B: there is one or more covered windows and none of them are detours. We've found our covered base windows. Hence, this would be the last iteration of this loop.\n\t\tcurrent_window = coverable_windows[#coverable_windows][1]\n\tend\n\n\tif #coverable_windows == 0 then\n\t\treturn { window }\n\tend\n\n\treturn vim.iter(coverable_windows)\n\t\t:flatten()\n\t\t:filter(function(other)\n\t\t\treturn util.overlap(other, window)\n\t\tend)\n\t\t:totable()\nend\n\n---@param window_id integer\n---@return boolean\nfunction util.is_open(window_id)\n\treturn vim.tbl_contains(vim.api.nvim_list_wins(), window_id)\nend\n\n---@param number integer\n---@return string\nfunction util.stringify(number)\n\tlocal base = string.byte(\"a\")\n\tlocal values = {}\n\tfor digit in (\"\" .. number):gmatch(\".\") do\n\t\tvalues[#values + 1] = tonumber(digit) + base\n\tend\n\treturn string.char(unpack(values))\nend\n\n---@param t table\n---@param f? fun(a:any,b:any):boolean\n---@return fun(): any, any\nfunction util.pairs_by_keys(t, f)\n\tlocal a = {}\n\tfor n in pairs(t) do\n\t\ttable.insert(a, n)\n\tend\n\ttable.sort(a, f)\n\tlocal i = 0 -- iterator variable\n\tlocal iter = function() -- iterator function\n\t\ti = i + 1\n\t\tif a[i] == nil then\n\t\t\treturn nil\n\t\telse\n\t\t\treturn a[i], t[a[i]]\n\t\tend\n\tend\n\treturn iter\nend\n\n---Whether the statusline is global (laststatus == 3).\n---@return boolean\nfunction util.is_statusline_global()\n\t-- When laststatus == 3, Neovim uses a single global statusline\n\t-- and individual windows do not have their own.\n\treturn vim.o.laststatus == 3\nend\n\n---Find non-floating window at a given screen position (1-based)\n---@param tab_id integer\n---@param screenrow integer\n---@param screencol integer\n---@return integer|nil\nfunction util.base_at_screenpos(tab_id, screenrow, screencol)\n\tfor _, win in ipairs(vim.api.nvim_tabpage_list_wins(tab_id)) do\n\t\tif not util.is_floating(win) then\n\t\t\tlocal winnr = vim.fn.win_id2win(win)\n\t\t\tassert(winnr ~= 0, tostring(win) .. \" was not found\")\n\t\t\tlocal top, left = unpack(vim.fn.win_screenpos(winnr))\n\t\t\tassert(top ~= 0 and left ~= 0, tostring(winnr) .. \" was not found\")\n\t\t\tlocal width = vim.fn.winwidth(winnr) -- includes signs/number column\n\t\t\tlocal height = vim.fn.winheight(winnr)\n\t\t\tif\n\t\t\t\tscreenrow >= top\n\t\t\t\tand screenrow\n\t\t\t\t\t< top\n\t\t\t\t\t\t+ height\n\t\t\t\t\t\t+ (util.is_statusline_global() and 0 or 1) -- for per-window statusline\n\t\t\t\t\t\t+ 1 -- for the border and global statusline\n\t\t\t\tand screencol >= left\n\t\t\t\tand screencol < left + width\n\t\t\tthen\n\t\t\t\treturn win\n\t\t\tend\n\t\tend\n\tend\n\treturn nil\nend\n\nreturn util\n"
  },
  {
    "path": "lua/detour/windowing_algorithm.lua",
    "content": "---@mod detour.windowing_algorithm\n---Layout algorithm for computing popup positions.\n\nlocal algo = {}\n\nlocal util = require(\"detour.util\")\n\n---Compute float options to cover the given windows without overlapping others.\n---@param coverable_windows integer[]\n---@param tab_id integer\n---@return table|nil\nfunction algo.construct_window_opts(coverable_windows, tab_id)\n\tlocal roots = {}\n\tfor _, window_id in ipairs(vim.api.nvim_tabpage_list_wins(tab_id)) do\n\t\tif not util.is_floating(window_id) then\n\t\t\ttable.insert(roots, window_id)\n\t\tend\n\tend\n\n\tlocal uncoverable_windows = {}\n\tfor _, root in ipairs(roots) do\n\t\tif not util.contains_element(coverable_windows, root) then\n\t\t\ttable.insert(uncoverable_windows, root)\n\t\tend\n\tend\n\tlocal horizontals = {}\n\tlocal verticals = {}\n\n\tfor _, root in ipairs(roots) do\n\t\tlocal top, bottom, left, right = util.get_text_area_dimensions(root)\n\t\thorizontals[top] = 1\n\t\thorizontals[bottom] = 1\n\t\tverticals[left] = 1\n\t\tverticals[right] = 1\n\tend\n\n\tlocal floors = {}\n\tlocal sides = {}\n\n\tfor top, _ in pairs(horizontals) do\n\t\tfor bottom, _ in pairs(horizontals) do\n\t\t\tif top < bottom then\n\t\t\t\ttable.insert(floors, { top, bottom })\n\t\t\tend\n\t\tend\n\tend\n\n\tfor left, _ in pairs(verticals) do\n\t\tfor right, _ in pairs(verticals) do\n\t\t\tif left < right then\n\t\t\t\ttable.insert(sides, { left, right })\n\t\t\tend\n\t\tend\n\tend\n\n\tlocal max_area = 0\n\tlocal dimensions = nil\n\tfor _, curr_floors in ipairs(floors) do\n\t\tlocal top, bottom = unpack(curr_floors)\n\t\tfor _, curr_sides in ipairs(sides) do\n\t\t\tlocal left, right = unpack(curr_sides)\n\t\t\tlocal legal = true\n\t\t\tfor _, uncoverable_window in ipairs(uncoverable_windows) do\n\t\t\t\tlocal uncoverable_top, uncoverable_bottom, uncoverable_left, uncoverable_right =\n\t\t\t\t\tutil.get_text_area_dimensions(uncoverable_window)\n\t\t\t\tif not util.is_statusline_global() then -- we have to worry about statuslines\n\t\t\t\t\t-- The fact that we're starting with text area dimensions\n\t\t\t\t\t-- means that all of the rectangles we are working with do\n\t\t\t\t\t-- not include statuslines. This means that we need to avoid\n\t\t\t\t\t-- inadvertantly covering a window's status line. Neovim can\n\t\t\t\t\t-- be configured to only show statuslines when there are\n\t\t\t\t\t-- multiple windows on the screen but we can ignore that\n\t\t\t\t\t-- because this loop only runs when there are multiple\n\t\t\t\t\t-- windows on the screen.\n\t\t\t\t\tuncoverable_top = uncoverable_top - 1 -- don't cover above window's statusline\n\t\t\t\t\tuncoverable_bottom = uncoverable_bottom + 1 -- don't cover this window's statusline\n\t\t\t\tend\n\t\t\t\tlocal lowest_top = math.max(top, uncoverable_top)\n\t\t\t\tlocal highest_bottom = math.min(bottom, uncoverable_bottom)\n\t\t\t\tlocal rightest_left = math.max(left, uncoverable_left)\n\t\t\t\tlocal leftest_right = math.min(right, uncoverable_right)\n\t\t\t\tif\n\t\t\t\t\t(lowest_top < highest_bottom)\n\t\t\t\t\tand (rightest_left < leftest_right)\n\t\t\t\tthen\n\t\t\t\t\tlegal = false\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tlocal area = (bottom - top) * (right - left)\n\t\t\tif legal and (area > max_area) then\n\t\t\t\tdimensions = { top, bottom, left, right }\n\t\t\t\tmax_area = area\n\t\t\tend\n\t\tend\n\tend\n\n\tif dimensions == nil then\n\t\tvim.api.nvim_echo(\n\t\t\t{ { \"[detour.nvim] was unable to find a spot to create a popup.\" } },\n\t\t\ttrue,\n\t\t\t{ err = true }\n\t\t)\n\t\treturn nil\n\tend\n\n\tlocal top, bottom, left, right = unpack(dimensions)\n\tlocal width = right - left\n\tlocal height = bottom - top\n\n\tif height < 1 then\n\t\tvim.api.nvim_echo({\n\t\t\t{\n\t\t\t\t\"[detour.nvim] (please file a github issue!) height is supposed to be at least 1.\",\n\t\t\t},\n\t\t}, true, { err = true })\n\t\treturn nil\n\tend\n\tif width < 1 then\n\t\tvim.api.nvim_echo({\n\t\t\t{\n\t\t\t\t\"[detour.nvim] (please file a github issue!) width is supposed to be at least 1.\",\n\t\t\t},\n\t\t}, true, { err = true })\n\t\treturn nil\n\tend\n\n\t-- If a window's height extends below the UI, the window's border gets cut off.\n\t-- If a window's width extends beyond the UI, the window's border still shows up at the end of the UI.\n\t-- Using a border adds 2 to the window's height and width.\n\tlocal window_opts = {\n\t\trelative = \"editor\",\n\t\trow = top,\n\t\tcol = left,\n\t\twidth = (width - 2 > 0) and (width - 2) or width, -- create some space for borders\n\t\theight = (height - 2 > 0) and (height - 2) or height, -- create some space for borders\n\t\tborder = \"rounded\",\n\t\tzindex = 1,\n\t}\n\n\tif window_opts.width > 4 then\n\t\twindow_opts.width = window_opts.width - 4\n\t\twindow_opts.col = window_opts.col + 2\n\tend\n\n\tif window_opts.height > 2 then\n\t\twindow_opts.height = window_opts.height - 2\n\t\twindow_opts.row = window_opts.row + 1\n\tend\n\n\treturn window_opts\nend\n\n---Construct nested float window opts within a parent float.\n---@param parent integer\n---@param layer integer?\n---@return table\nfunction algo.construct_nest(parent, layer)\n\tassert(\n\t\tutil.is_open(parent),\n\t\t\"trying to construct a nested window in a window that doesn't exist: \",\n\t\tparent\n\t)\n\tlocal top, bottom, left, right = util.get_text_area_dimensions(parent)\n\tlocal width = right - left\n\tlocal height = bottom - top\n\tlocal border = \"rounded\"\n\tif height >= 3 then\n\t\theight = height - 2\n\t\ttop = top + 1\n\tend\n\tif width >= 3 then\n\t\twidth = width - 2\n\t\tleft = left + 1\n\tend\n\treturn {\n\t\trelative = \"editor\",\n\t\trow = top,\n\t\tcol = left,\n\t\twidth = width,\n\t\theight = height,\n\t\tborder = border,\n\t\tzindex = layer,\n\t}\nend\n\nreturn algo\n"
  },
  {
    "path": "plugin/detour.lua",
    "content": "local detour = require(\"detour\")\nlocal features = require(\"detour.features\")\n\nvim.api.nvim_create_user_command(\"Detour\", detour.Detour, {})\n\nvim.api.nvim_create_user_command(\n\t\"DetourCurrentWindow\",\n\tdetour.DetourCurrentWindow,\n\t{}\n)\n\nvim.api.nvim_create_user_command(\n\t\"DetourUncoverWindowWithMouse\",\n\tfeatures.UncoverWindowWithMouse,\n\t{}\n)\n\nvim.api.nvim_create_user_command(\n\t\"DetourHideAllDetours\",\n\tfeatures.HideAllDetours,\n\t{}\n)\n\nvim.api.nvim_create_user_command(\n\t\"DetourRevealAllDetours\",\n\tfeatures.RevealAllDetours,\n\t{}\n)\n\nvim.api.nvim_create_user_command(\n\t\"DetourCloseCurrentStack\",\n\tfeatures.CloseCurrentStack,\n\t{}\n)\n"
  },
  {
    "path": "run-in-docker.sh",
    "content": "#!/bin/sh\nnvim -u NONE \\\n  -c \"lua local k,l,_=pcall(require,'luarocks.loader') _=k and l.add_context('busted','$BUSTED_VERSION')\" \\\n  -l \"/usr/local/lib/luarocks/rocks-5.1/busted/$BUSTED_VERSION/bin/busted\" .\n"
  },
  {
    "path": "spec/config_spec.lua",
    "content": "local detour = require(\"detour\")\nlocal config = require(\"detour.config\")\n\ndescribe(\"detour config\", function()\n\tbefore_each(function()\n\t\tvim.g.detour_testing = true\n\t\tvim.cmd([[ \n      %bwipeout!\n      mapclear\n      nmapclear\n      vmapclear\n      xmapclear\n      smapclear\n      omapclear\n      mapclear\n      imapclear\n      lmapclear\n      cmapclear\n      tmapclear\n    ]])\n\t\tvim.api.nvim_clear_autocmds({}) -- delete any autocmds not in a group\n\t\tfor _, autocmd in ipairs(vim.api.nvim_get_autocmds({})) do\n\t\t\tif vim.startswith(autocmd.group_name, \"detour-\") then\n\t\t\t\tvim.api.nvim_del_autocmd(autocmd.id)\n\t\t\tend\n\t\tend\n\t\tvim.o.splitbelow = true\n\t\tvim.o.splitright = true\n\t\t-- Reset to default configuration for each test\n\t\tconfig.setup({ title = \"path\" })\n\tend)\n\n\tit(\"disables titles when title = 'none'\", function()\n\t\tconfig.setup({ title = \"none\" })\n\t\tassert(detour.Detour())\n\t\t-- With title disabled, floating window should not have a title\n\t\tassert.is_nil(vim.api.nvim_win_get_config(0).title)\n\tend)\n\n\tit(\"rejects invalid options and keeps previous config\", function()\n\t\tlocal before = config.options.title\n\t\tconfig.setup({ title = \"not-a-valid-option\" })\n\t\tassert.same(before, config.options.title)\n\tend)\nend)\n"
  },
  {
    "path": "spec/detour_auto_unreserve_spec.lua",
    "content": "local detour = require(\"detour\")\nlocal util = require(\"detour.util\")\n\ndescribe(\"detour auto-unreserve on interaction\", function()\n\tbefore_each(function()\n\t\tvim.g.detour_testing = true\n\t\tvim.cmd([[ \n      %bwipeout!\n      mapclear\n      nmapclear\n      vmapclear\n      xmapclear\n      smapclear\n      omapclear\n      mapclear\n      imapclear\n      lmapclear\n      cmapclear\n      tmapclear\n    ]])\n\t\tvim.api.nvim_clear_autocmds({}) -- delete any autocmds not in a group\n\t\tfor _, autocmd in ipairs(vim.api.nvim_get_autocmds({ pattern = \"*\" })) do\n\t\t\tif vim.startswith(autocmd.group_name, \"detour-\") then\n\t\t\t\tvim.api.nvim_del_autocmd(autocmd.id)\n\t\t\tend\n\t\tend\n\t\tvim.o.splitbelow = true\n\t\tvim.o.splitright = true\n\tend)\n\n\tit(\"unreserves interacted window and resizes popup\", function()\n\t\t-- Create a 2-column layout\n\t\tlocal left_base = vim.api.nvim_get_current_win()\n\t\tvim.cmd.vsplit()\n\t\tlocal right_base = vim.api.nvim_get_current_win()\n\n\t\t-- Create a detour covering both base windows\n\t\tlocal popup = assert(detour.Detour())\n\t\tassert.True(util.overlap(popup, right_base))\n\n\t\t-- Focus the left base window, then simulate user interaction\n\t\tvim.cmd.split()\n\t\tvim.api.nvim_exec_autocmds(\"VimResized\", {}) -- trigger detour resize\n\t\tassert.False(util.overlap(popup, right_base))\n\t\tvim.fn.win_gotoid(right_base)\n\t\tvim.cmd.startinsert() -- right_base should now be unreserved\n\n\t\tassert.are.same(util.find_covered_windows(popup), { left_base })\n\tend)\nend)\n"
  },
  {
    "path": "spec/detour_close_stack_spec.lua",
    "content": "local detour = require(\"detour\")\nlocal util = require(\"detour.util\")\nlocal features = require(\"detour.features\")\n\ndescribe(\"features.CloseCurrentStack\", function()\n\tbefore_each(function()\n\t\tvim.g.detour_testing = true\n\t\tvim.cmd([[\n      %bwipeout!\n      mapclear\n      nmapclear\n      vmapclear\n      xmapclear\n      smapclear\n      omapclear\n      mapclear\n      imapclear\n      lmapclear\n      cmapclear\n      tmapclear\n    ]])\n\t\tvim.api.nvim_clear_autocmds({}) -- delete any autocmds not in a group\n\t\tfor _, autocmd in ipairs(vim.api.nvim_get_autocmds({ pattern = \"*\" })) do\n\t\t\tif vim.startswith(autocmd.group_name, \"detour-\") then\n\t\t\t\tvim.api.nvim_del_autocmd(autocmd.id)\n\t\t\tend\n\t\tend\n\t\tvim.o.splitbelow = true\n\t\tvim.o.splitright = true\n\tend)\n\n\tit(\"closes a single detour and returns to base\", function()\n\t\tlocal base = vim.api.nvim_get_current_win()\n\t\tlocal popup = assert(detour.Detour())\n\n\t\tfeatures.CloseCurrentStack()\n\n\t\tassert.False(util.is_open(popup))\n\t\tassert.same({ base }, vim.api.nvim_list_wins())\n\tend)\n\n\tit(\"closes all nested detours in the chain\", function()\n\t\tlocal original = vim.api.nvim_get_current_win()\n\t\tvim.cmd.split()\n\t\tlocal base = vim.api.nvim_get_current_win()\n\t\tlocal parent = assert(detour.Detour())\n\t\tlocal child = assert(detour.Detour())\n\t\tlocal grandchild = assert(detour.Detour())\n\n\t\tfeatures.CloseCurrentStack()\n\n\t\tassert.False(util.is_open(parent))\n\t\tassert.False(util.is_open(child))\n\t\tassert.False(util.is_open(grandchild))\n\t\tassert.same({ original, base }, vim.api.nvim_list_wins())\n\t\tvim.api.nvim_win_close(base, true)\n\tend)\n\n\tit(\"is a no-op when not inside a detour\", function()\n\t\tvim.cmd.split()\n\t\tlocal base = vim.api.nvim_get_current_win()\n\t\tlocal before = vim.api.nvim_list_wins()\n\n\t\tfeatures.CloseCurrentStack()\n\n\t\tlocal after = vim.api.nvim_list_wins()\n\t\tassert.same(before, after)\n\t\tassert.same(base, vim.api.nvim_get_current_win())\n\t\tvim.cmd.close()\n\tend)\nend)\n"
  },
  {
    "path": "spec/detour_hide_reveal_spec.lua",
    "content": "local detour = require(\"detour\")\nlocal util = require(\"detour.util\")\nlocal features = require(\"detour.features\")\n\ndescribe(\"detour hide/reveal\", function()\n\tbefore_each(function()\n\t\tvim.g.detour_testing = true\n\t\tvim.cmd([[\n        %bwipeout!\n        mapclear\n        nmapclear\n        vmapclear\n        xmapclear\n        smapclear\n        omapclear\n        mapclear\n        imapclear\n        lmapclear\n        cmapclear\n        tmapclear\n        ]])\n\t\tvim.api.nvim_clear_autocmds({})\n\t\tfor _, autocmd in ipairs(vim.api.nvim_get_autocmds({ pattern = \"*\" })) do\n\t\t\tif vim.startswith(autocmd.group_name, \"detour-\") then\n\t\t\t\tvim.api.nvim_del_autocmd(autocmd.id)\n\t\t\tend\n\t\tend\n\t\tvim.o.splitbelow = true\n\t\tvim.o.splitright = true\n\tend)\n\n\tit(\"HideAllDetours and RevealAllDetours toggle popup visibility\", function()\n\t\t-- Create two side-by-side base windows\n\t\tvim.cmd.vsplit()\n\t\tvim.cmd.wincmd(\"h\")\n\t\tlocal left_base = vim.api.nvim_get_current_win()\n\t\tvim.cmd.wincmd(\"l\")\n\t\tlocal right_base = vim.api.nvim_get_current_win()\n\n\t\t-- Create a detour over each base window\n\t\tvim.fn.win_gotoid(right_base)\n\t\tlocal right_popup = assert(detour.DetourCurrentWindow())\n\t\tvim.fn.win_gotoid(left_base)\n\t\tlocal left_popup = assert(detour.DetourCurrentWindow())\n\n\t\t-- Initially, popups are visible\n\t\tassert.False(vim.api.nvim_win_get_config(left_popup).hide or false)\n\t\tassert.False(vim.api.nvim_win_get_config(right_popup).hide or false)\n\n\t\t-- Hide all detours\n\t\tfeatures.HideAllDetours()\n\t\tassert.True(vim.api.nvim_win_get_config(left_popup).hide)\n\t\tassert.True(vim.api.nvim_win_get_config(right_popup).hide)\n\n\t\t-- Reveal all detours\n\t\tfeatures.RevealAllDetours()\n\t\tassert.False(vim.api.nvim_win_get_config(left_popup).hide)\n\t\tassert.False(vim.api.nvim_win_get_config(right_popup).hide)\n\tend)\nend)\n"
  },
  {
    "path": "spec/detour_movements_spec.lua",
    "content": "local detour = require(\"detour\")\nlocal movements = require(\"detour.movements\")\nlocal util = require(\"detour.util\")\n\nfunction Set(list)\n\tlocal set = {}\n\tfor _, l in ipairs(list) do\n\t\tset[l] = true\n\tend\n\treturn set\nend\n\ndescribe(\"detour\", function()\n\tbefore_each(function()\n\t\tvim.cmd([[\n        %bwipeout!\n        mapclear\n        nmapclear\n        vmapclear\n        xmapclear\n        smapclear\n        omapclear\n        mapclear\n        imapclear\n        lmapclear\n        cmapclear\n        tmapclear\n        ]])\n\t\tvim.api.nvim_clear_autocmds({}) -- delete any autocmds not in a group\n\t\tfor _, autocmd in ipairs(vim.api.nvim_get_autocmds({ pattern = \"*\" })) do\n\t\t\tif vim.startswith(autocmd.group_name, \"detour-\") then\n\t\t\t\tvim.api.nvim_del_autocmd(autocmd.id)\n\t\t\tend\n\t\tend\n\t\tvim.o.splitbelow = true\n\t\tvim.o.splitright = true\n\tend)\n\n\tit(\"Switching horizontally between detours\", function()\n\t\tlocal left_base = vim.api.nvim_get_current_win()\n\n\t\tvim.cmd.vsplit()\n\t\tlocal middle_base = vim.api.nvim_get_current_win()\n\n\t\tvim.cmd.vsplit()\n\t\tlocal right_base = vim.api.nvim_get_current_win()\n\n\t\tvim.fn.win_gotoid(middle_base)\n\t\tlocal middle_popup = assert(detour.DetourCurrentWindow())\n\n\t\t-- Enter and leave a popup from a base window in both directions\n\t\tmovements.DetourWinCmdH()\n\t\tassert.same(vim.api.nvim_get_current_win(), left_base)\n\n\t\tmovements.DetourWinCmdL()\n\t\tassert.same(vim.api.nvim_get_current_win(), middle_popup)\n\n\t\tmovements.DetourWinCmdL()\n\t\tassert.same(vim.api.nvim_get_current_win(), right_base)\n\n\t\tmovements.DetourWinCmdH()\n\t\tassert.same(vim.api.nvim_get_current_win(), middle_popup)\n\n\t\t-- Create nested popup\n\t\tlocal middle_nested_popup = assert(detour.DetourCurrentWindow())\n\n\t\t-- Enter and leave a nested popup from a base window in both directions\n\t\tmovements.DetourWinCmdH()\n\t\tassert.same(vim.api.nvim_get_current_win(), left_base)\n\n\t\tmovements.DetourWinCmdL()\n\t\tassert.same(vim.api.nvim_get_current_win(), middle_nested_popup)\n\n\t\tmovements.DetourWinCmdL()\n\t\tassert.same(vim.api.nvim_get_current_win(), right_base)\n\n\t\tmovements.DetourWinCmdH()\n\t\tassert.same(vim.api.nvim_get_current_win(), middle_nested_popup)\n\n\t\t-- Create popups on left and right\n\t\tvim.fn.win_gotoid(left_base)\n\t\tlocal left_popup = assert(detour.DetourCurrentWindow())\n\n\t\tvim.fn.win_gotoid(right_base)\n\t\tlocal right_popup = assert(detour.DetourCurrentWindow())\n\n\t\tvim.fn.win_gotoid(middle_nested_popup)\n\n\t\t-- Enter and leave a nested popup from a non-nested popup in both directions\n\t\tmovements.DetourWinCmdH()\n\t\tassert.same(vim.api.nvim_get_current_win(), left_popup)\n\n\t\tmovements.DetourWinCmdL()\n\t\tassert.same(vim.api.nvim_get_current_win(), middle_nested_popup)\n\n\t\tmovements.DetourWinCmdL()\n\t\tassert.same(vim.api.nvim_get_current_win(), right_popup)\n\n\t\tmovements.DetourWinCmdH()\n\t\tassert.same(vim.api.nvim_get_current_win(), middle_nested_popup)\n\tend)\n\n\tit(\"Switching vertically between detours\", function()\n\t\tlocal top_base = vim.api.nvim_get_current_win()\n\n\t\tvim.cmd.split()\n\t\tlocal middle_base = vim.api.nvim_get_current_win()\n\n\t\tvim.cmd.split()\n\t\tlocal bottom_base = vim.api.nvim_get_current_win()\n\n\t\tvim.fn.win_gotoid(middle_base)\n\t\tlocal middle_popup = assert(detour.DetourCurrentWindow())\n\n\t\t-- Enter and leave a popup from a base window in both directions\n\t\tmovements.DetourWinCmdK()\n\t\tassert.same(vim.api.nvim_get_current_win(), top_base)\n\n\t\tmovements.DetourWinCmdJ()\n\t\tassert.same(vim.api.nvim_get_current_win(), middle_popup)\n\n\t\tmovements.DetourWinCmdJ()\n\t\tassert.same(vim.api.nvim_get_current_win(), bottom_base)\n\n\t\tmovements.DetourWinCmdK()\n\t\tassert.same(vim.api.nvim_get_current_win(), middle_popup)\n\n\t\t-- Create nested popup\n\t\tlocal middle_nested_popup = assert(detour.DetourCurrentWindow())\n\n\t\t-- Enter and leave a nested popup from a base window in both directions\n\t\tmovements.DetourWinCmdK()\n\t\tassert.same(vim.api.nvim_get_current_win(), top_base)\n\n\t\tmovements.DetourWinCmdJ()\n\t\tassert.same(vim.api.nvim_get_current_win(), middle_nested_popup)\n\n\t\tmovements.DetourWinCmdJ()\n\t\tassert.same(vim.api.nvim_get_current_win(), bottom_base)\n\n\t\tmovements.DetourWinCmdK()\n\t\tassert.same(vim.api.nvim_get_current_win(), middle_nested_popup)\n\n\t\t-- Create popups on top and bottom\n\t\tvim.fn.win_gotoid(top_base)\n\t\tlocal top_popup = assert(detour.DetourCurrentWindow())\n\n\t\tvim.fn.win_gotoid(bottom_base)\n\t\tlocal bottom_popup = assert(detour.DetourCurrentWindow())\n\n\t\tvim.fn.win_gotoid(middle_nested_popup)\n\n\t\t-- Enter and leave a nested popup from a non-nested popup in both directions\n\t\tmovements.DetourWinCmdK()\n\t\tassert.same(vim.api.nvim_get_current_win(), top_popup)\n\n\t\tmovements.DetourWinCmdJ()\n\t\tassert.same(vim.api.nvim_get_current_win(), middle_nested_popup)\n\n\t\tmovements.DetourWinCmdJ()\n\t\tassert.same(vim.api.nvim_get_current_win(), bottom_popup)\n\n\t\tmovements.DetourWinCmdK()\n\t\tassert.same(vim.api.nvim_get_current_win(), middle_nested_popup)\n\tend)\n\n\tit(\"Switch windows with <C-w>w\", function()\n\t\tlocal base = vim.api.nvim_get_current_win()\n\t\tlocal popup = detour.Detour()\n\n\t\tvim.cmd.split()\n\t\tlocal bottom_base = vim.api.nvim_get_current_win()\n\t\tassert.is_not.same(bottom_base, popup)\n\n\t\tmovements.DetourWinCmdW()\n\n\t\tassert.same(popup, vim.api.nvim_get_current_win())\n\n\t\tmovements.DetourWinCmdW()\n\t\tassert.same(bottom_base, vim.api.nvim_get_current_win())\n\tend)\n\n\tit(\"Move cursor on SafeState\", function()\n\t\tlocal base = vim.api.nvim_get_current_win()\n\t\tlocal popup = detour.Detour()\n\n\t\tvim.opt.eventignore = \"all\" -- deactivate plugin\n\t\tvim.api.nvim_set_current_win(base)\n\t\tvim.opt.eventignore = \"\" -- reactivate plugin\n\t\tmovements._safe_state_handler()\n\t\tassert.same(popup, vim.api.nvim_get_current_win())\n\tend)\nend)\n"
  },
  {
    "path": "spec/detour_spec.lua",
    "content": "local detour = require(\"detour\")\nlocal util = require(\"detour.util\")\nlocal features = require(\"detour.features\")\n\nfunction Set(list)\n\tlocal set = {}\n\tfor _, l in ipairs(list) do\n\t\tset[l] = true\n\tend\n\treturn set\nend\n\ndescribe(\"detour\", function()\n\tbefore_each(function()\n\t\tvim.g.detour_testing = true\n\t\tvim.cmd([[\n        %bwipeout!\n        mapclear\n        nmapclear\n        vmapclear\n        xmapclear\n        smapclear\n        omapclear\n        mapclear\n        imapclear\n        lmapclear\n        cmapclear\n        tmapclear\n        ]])\n\t\tvim.api.nvim_clear_autocmds({}) -- delete any autocmds not in a group\n\t\tfor _, autocmd in ipairs(vim.api.nvim_get_autocmds({})) do\n\t\t\tif vim.startswith(autocmd.group_name, \"detour-\") then\n\t\t\t\tvim.api.nvim_del_autocmd(autocmd.id)\n\t\t\tend\n\t\tend\n\t\tvim.o.splitbelow = true\n\t\tvim.o.splitright = true\n\tend)\n\n\t-- See Issue #25 for a discussion of duplication in this test suite.\n\tit(\"create popup\", function()\n\t\tvim.cmd.view(\"/tmp/detour\")\n\t\tlocal before_buffer = vim.api.nvim_get_current_buf()\n\t\tlocal before_window = vim.api.nvim_get_current_win()\n\n\t\tlocal after_window = assert(detour.Detour())\n\t\tlocal after_buffer = vim.api.nvim_get_current_buf()\n\t\tassert.are_not.same(before_window, after_window)\n\t\tassert.are.same(before_buffer, after_buffer)\n\t\tassert.True(util.is_floating(after_window))\n\t\tassert.same(#vim.api.nvim_list_wins(), 2)\n\tend)\n\n\tit(\"create nested popup over non-Detour popup\", function()\n\t\tlocal base_buffer = vim.api.nvim_get_current_buf()\n\t\tvim.api.nvim_open_win(\n\t\t\tvim.api.nvim_win_get_buf(0),\n\t\t\ttrue,\n\t\t\t{ relative = \"win\", width = 12, height = 3, bufpos = { 100, 10 } }\n\t\t)\n\n\t\t-- See the note above regarding the duplication of this test code.\n\t\tlocal parent_popup = vim.api.nvim_get_current_win()\n\t\tlocal parent_buffer = vim.api.nvim_get_current_buf()\n\t\tassert.True(util.is_floating(parent_popup))\n\t\tlocal parent_config = vim.api.nvim_win_get_config(parent_popup)\n\n\t\tlocal child_popup = assert(detour.Detour())\n\t\tassert.True(util.is_floating(child_popup))\n\t\tlocal child_config = vim.api.nvim_win_get_config(child_popup)\n\t\tlocal child_buffer = vim.api.nvim_get_current_buf()\n\t\t-- The nested popup should always be on top of the parent popup\n\t\tassert.True(child_config.zindex > parent_config.zindex)\n\n\t\tassert.same(#vim.api.nvim_list_wins(), 3)\n\t\tassert.same(base_buffer, parent_buffer)\n\t\tassert.same(parent_buffer, child_buffer)\n\n\t\tvim.cmd.quit()\n\t\tassert.same(#vim.api.nvim_list_wins(), 2)\n\t\tvim.cmd.quit()\n\t\tassert.same(#vim.api.nvim_list_wins(), 1)\n\tend)\n\n\t-- TODO: Make sure popups are fully contained within their parents\n\tit(\"create nested popup\", function()\n\t\tlocal base_buffer = vim.api.nvim_get_current_buf()\n\n\t\tlocal parent_popup = assert(detour.Detour())\n\t\tlocal parent_buffer = vim.api.nvim_get_current_buf()\n\t\tassert.True(util.is_floating(parent_popup))\n\t\tlocal parent_config = vim.api.nvim_win_get_config(parent_popup)\n\n\t\tlocal child_popup = assert(detour.Detour())\n\t\tassert.True(util.is_floating(child_popup))\n\t\tlocal child_config = vim.api.nvim_win_get_config(child_popup)\n\t\tlocal child_buffer = vim.api.nvim_get_current_buf()\n\t\t-- The nested popup should always be on top of the parent popup\n\t\tassert.True(child_config.zindex > parent_config.zindex)\n\n\t\tassert.same(#vim.api.nvim_list_wins(), 3)\n\t\tassert.same(base_buffer, parent_buffer)\n\t\tassert.same(parent_buffer, child_buffer)\n\n\t\tvim.cmd.quit()\n\t\tassert.same(#vim.api.nvim_list_wins(), 2)\n\t\tvim.cmd.quit()\n\t\tassert.same(#vim.api.nvim_list_wins(), 1)\n\tend)\n\n\tit(\"react to a coverable window closing\", function()\n\t\tvim.cmd.wincmd(\"v\")\n\t\tlocal coverable_window = vim.api.nvim_get_current_win()\n\t\tlocal popup = assert(detour.Detour())\n\t\tassert.True(util.overlap(popup, coverable_window))\n\t\tvim.fn.win_gotoid(coverable_window)\n\t\tvim.cmd.wincmd(\"s\")\n\t\tlocal uncoverable_win = vim.api.nvim_get_current_win()\n\t\tvim.api.nvim_exec_autocmds(\n\t\t\t\"WinResized\",\n\t\t\t{ data = { windows = { coverable_window } } }\n\t\t)\n\t\tassert.False(util.overlap(popup, uncoverable_win))\n\t\tvim.api.nvim_win_close(coverable_window, true)\n\n\t\tassert.False(util.overlap(popup, uncoverable_win))\n\t\tvim.api.nvim_win_close(uncoverable_win, true)\n\tend)\n\n\tit(\"create popup over current window\", function()\n\t\tlocal window_a = vim.api.nvim_get_current_win()\n\t\tvim.cmd.wincmd(\"s\")\n\t\tlocal window_b = vim.api.nvim_get_current_win()\n\t\tlocal popup_b = assert(detour.DetourCurrentWindow())\n\t\tassert.False(util.overlap(window_a, popup_b))\n\t\tassert.True(util.overlap(window_b, popup_b))\n\t\tvim.fn.win_gotoid(window_a)\n\t\tlocal popup_a = assert(detour.Detour())\n\t\tassert.True(util.overlap(window_a, popup_a))\n\t\tassert.False(util.overlap(window_b, popup_a))\n\tend)\n\n\tit(\"Do not allow two popups over the same window\", function()\n\t\tlocal win = vim.api.nvim_get_current_win()\n\t\tlocal popup = assert(detour.Detour())\n\t\tvim.fn.win_gotoid(win)\n\t\tassert.Nil(detour.Detour())\n\t\tassert.same(Set({ win, popup }), Set(vim.api.nvim_tabpage_list_wins(0)))\n\n\t\tvim.fn.win_gotoid(win)\n\t\tassert.Nil(detour.DetourCurrentWindow())\n\t\tassert.same(Set({ win, popup }), Set(vim.api.nvim_tabpage_list_wins(0)))\n\tend)\n\n\tit(\n\t\t\"Do not allow two 'current window' popups over the same window\",\n\t\tfunction()\n\t\t\tlocal win = vim.api.nvim_get_current_win()\n\t\t\tlocal popup = assert(detour.DetourCurrentWindow())\n\t\t\tvim.fn.win_gotoid(win)\n\t\t\tassert.Nil(detour.DetourCurrentWindow())\n\t\t\tassert.same(\n\t\t\t\tSet({ win, popup }),\n\t\t\t\tSet(vim.api.nvim_tabpage_list_wins(0))\n\t\t\t)\n\n\t\t\tvim.fn.win_gotoid(win)\n\t\t\tassert.Nil(detour.Detour())\n\t\t\tassert.same(\n\t\t\t\tSet({ win, popup }),\n\t\t\t\tSet(vim.api.nvim_tabpage_list_wins(0))\n\t\t\t)\n\t\tend\n\t)\n\n\tit(\"Switch focus to a popup's floating parent when it's closed\", function()\n\t\tvim.api.nvim_open_win(\n\t\t\tvim.api.nvim_win_get_buf(0),\n\t\t\ttrue,\n\t\t\t{ relative = \"win\", width = 12, height = 3, bufpos = { 100, 10 } }\n\t\t)\n\n\t\tlocal wins = { vim.api.nvim_get_current_win() }\n\t\tfor _ = 1, 10 do\n\t\t\ttable.insert(wins, detour.Detour())\n\t\t\tfor j, win in ipairs(wins) do\n\t\t\t\tassert(win)\n\t\t\t\tassert.same(\n\t\t\t\t\tvim.api.nvim_win_get_config(win).focusable,\n\t\t\t\t\tj == #wins\n\t\t\t\t)\n\t\t\tend\n\t\tend\n\n\t\tfor _ = 1, 10 do\n\t\t\ttable.remove(wins, #wins)\n\t\t\tvim.cmd.close()\n\t\t\tassert.same(vim.api.nvim_get_current_win(), wins[#wins])\n\t\t\tfor j, win in ipairs(wins) do\n\t\t\t\tassert.same(\n\t\t\t\t\tvim.api.nvim_win_get_config(win).focusable,\n\t\t\t\t\tj == #wins\n\t\t\t\t)\n\t\t\tend\n\t\tend\n\tend)\n\n\tit(\"Switch focus to a popup's parent when it's closed\", function()\n\t\tlocal wins = { vim.api.nvim_get_current_win() }\n\t\tfor _ = 1, 10 do\n\t\t\ttable.insert(wins, detour.Detour())\n\t\t\tfor j, win in ipairs(wins) do\n\t\t\t\tassert(win)\n\t\t\t\tif j > 1 then -- the base window cannot be unfocusable\n\t\t\t\t\tassert.same(\n\t\t\t\t\t\tvim.api.nvim_win_get_config(win).focusable,\n\t\t\t\t\t\tj == #wins\n\t\t\t\t\t)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\n\t\tfor _ = 1, 10 do\n\t\t\ttable.remove(wins, #wins)\n\t\t\tvim.cmd.close()\n\t\t\tassert.same(vim.api.nvim_get_current_win(), wins[#wins])\n\t\t\tfor j, win in ipairs(wins) do\n\t\t\t\tif j > 1 then -- the base window cannot be unfocusable\n\t\t\t\t\tassert.same(\n\t\t\t\t\t\tvim.api.nvim_win_get_config(win).focusable,\n\t\t\t\t\t\tj == #wins\n\t\t\t\t\t)\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend)\n\n\tit(\n\t\t\"Handle cases when popups close without throwing a WinClosed event\",\n\t\tfunction()\n\t\t\tlocal popup = assert(detour.DetourCurrentWindow())\n\t\t\tvim.api.nvim_create_autocmd({ \"WinLeave\" }, {\n\t\t\t\tcallback = function()\n\t\t\t\t\tvim.api.nvim_win_close(0, true)\n\t\t\t\t\treturn true\n\t\t\t\tend,\n\t\t\t})\n\t\t\tvim.cmd.wincmd(\"h\") -- Close popup without WinClosed event\n\t\t\tassert.False(util.is_open(popup))\n\t\t\tassert(detour.DetourCurrentWindow())\n\t\tend\n\t)\n\n\tit(\"Test CloseOnLeave\", function()\n\t\tlocal popup_id = assert(detour.Detour())\n\t\tfeatures.CloseOnLeave(popup_id)\n\t\tassert.True(util.is_open(popup_id))\n\t\tvim.cmd.wincmd(\"w\")\n\t\tassert.False(util.is_open(popup_id))\n\n\t\tpopup_id = assert(detour.Detour())\n\t\tfeatures.CloseOnLeave(popup_id)\n\t\tassert.True(util.is_open(popup_id))\n\t\tvim.cmd.split()\n\t\tassert.False(util.is_open(popup_id))\n\tend)\n\n\tit(\"Test ShowPathInTitle\", function()\n\t\tvim.cmd.file(\"/tmp/detour_test_a\")\n\t\tlocal popup_id = assert(detour.Detour())\n\t\tassert.same(\n\t\t\t\"/tmp/detour_test_a\",\n\t\t\tvim.api.nvim_win_get_config(0).title[1][1]\n\t\t)\n\t\tvim.cmd.file(\"/tmp/detour_test_b\")\n\t\t-- Simulate the event triggered by the `on_win` decorator\n\t\tvim.cmd.doautocmd(\"User DetourUpdateTitle\" .. util.stringify(popup_id))\n\t\tassert.same(\n\t\t\t\"/tmp/detour_test_b\",\n\t\t\tvim.api.nvim_win_get_config(0).title[1][1]\n\t\t)\n\tend)\nend)\n"
  },
  {
    "path": "spec/detour_uncover_spec.lua",
    "content": "local detour = require(\"detour\")\nlocal util = require(\"detour.util\")\nlocal features = require(\"detour.features\")\n\nlocal function Set(list)\n  local set = {}\n  for _, l in ipairs(list) do\n    set[l] = true\n  end\n  return set\nend\n\ndescribe(\"detour uncover feature\", function()\n  before_each(function()\n    vim.g.detour_testing = true\n    vim.cmd([[ \n      %bwipeout!\n      mapclear\n      nmapclear\n      vmapclear\n      xmapclear\n      smapclear\n      omapclear\n      mapclear\n      imapclear\n      lmapclear\n      cmapclear\n      tmapclear\n    ]])\n    vim.api.nvim_clear_autocmds({}) -- delete any autocmds not in a group\n    for _, autocmd in ipairs(vim.api.nvim_get_autocmds({ pattern = \"*\" })) do\n      if vim.startswith(autocmd.group_name, \"detour-\") then\n        vim.api.nvim_del_autocmd(autocmd.id)\n      end\n    end\n    vim.o.splitbelow = true\n    vim.o.splitright = true\n  end)\n\n  it(\"uncover one base window and resize popup accordingly\", function()\n    -- Create a simple 2-column layout\n    local left_base = vim.api.nvim_get_current_win()\n    vim.cmd.vsplit()\n    local right_base = vim.api.nvim_get_current_win()\n\n    -- Create a detour covering both base windows\n    local popup = assert(detour.Detour())\n\n    -- Sanity: popup overlaps both bases initially\n    assert.True(util.overlap(popup, left_base))\n    assert.True(util.overlap(popup, right_base))\n\n    -- Uncover the left base window\n    assert.True(features.UncoverWindow(left_base))\n\n    -- The popup should no longer overlap the left base\n    assert.False(util.overlap(popup, left_base))\n    -- The popup should still overlap the right base\n    assert.True(util.overlap(popup, right_base))\n\n    -- Covered windows should only include the right base now\n    local covered = Set(util.find_covered_windows(popup))\n    assert.True(covered[right_base])\n    assert.is_nil(covered[left_base])\n  end)\n\n  it(\"prevent uncovering the last remaining base window\", function()\n    -- Create a simple 2-column layout\n    local left_base = vim.api.nvim_get_current_win()\n    vim.cmd.vsplit()\n    local right_base = vim.api.nvim_get_current_win()\n\n    -- Create a detour covering both base windows\n    local popup = assert(detour.Detour())\n\n    -- Uncover one window first\n    assert.True(features.UncoverWindow(left_base))\n\n    -- Attempt to uncover the last remaining window should fail\n    assert.False(features.UncoverWindow(right_base))\n\n    -- State should remain unchanged: popup still overlaps right_base\n    assert.True(util.overlap(popup, right_base))\n    assert.False(util.overlap(popup, left_base))\n  end)\nend)\n\n"
  },
  {
    "path": "spec/internal_spec.lua",
    "content": "local detour = require(\"detour\")\nlocal internal = require(\"detour.internal\")\nlocal util = require(\"detour.util\")\n\ndescribe(\"detour internal\", function()\n\tbefore_each(function()\n\t\tvim.g.detour_testing = true\n\t\tvim.cmd([[ \n      %bwipeout!\n      mapclear\n      nmapclear\n      vmapclear\n      xmapclear\n      smapclear\n      omapclear\n      mapclear\n      imapclear\n      lmapclear\n      cmapclear\n      tmapclear\n    ]])\n\t\tvim.api.nvim_clear_autocmds({}) -- delete any autocmds not in a group\n\t\tfor _, autocmd in ipairs(vim.api.nvim_get_autocmds({})) do\n\t\t\tif vim.startswith(autocmd.group_name, \"detour-\") then\n\t\t\t\tvim.api.nvim_del_autocmd(autocmd.id)\n\t\t\tend\n\t\tend\n\t\tvim.o.splitbelow = true\n\t\tvim.o.splitright = true\n\tend)\n\n\tit(\"teardown is idempotent and clears reservations\", function()\n\t\tlocal popup = assert(detour.Detour())\n\t\tinternal.teardown_detour(popup)\n\t\tinternal.teardown_detour(popup)\n\t\tassert.is_nil(internal.get_reserved_windows(popup))\n\tend)\n\n\tit(\"garbage_collect removes closed popups\", function()\n\t\tlocal popup = assert(detour.Detour())\n\t\tvim.api.nvim_win_close(popup, true)\n\t\tinternal.garbage_collect()\n\t\tassert.False(vim.tbl_contains(internal.list_popups(), popup))\n\t\tassert.is_nil(internal.get_reserved_windows(popup))\n\tend)\n\n\tit(\"get_reserved_windows filters out closed windows\", function()\n\t\tvim.cmd.vsplit()\n\t\tlocal right = vim.api.nvim_get_current_win()\n\n\t\tlocal popup = assert(detour.Detour())\n\t\tassert.True(util.overlap(popup, right))\n\n\t\t-- Close one base and ensure it is removed from reservations\n\t\tvim.fn.win_gotoid(right)\n\t\tvim.api.nvim_win_close(right, true)\n\t\tlocal reserved = internal.get_reserved_windows(popup)\n\t\tassert.truthy(reserved)\n\t\tassert.False(vim.tbl_contains(reserved, right))\n\tend)\nend)\n"
  },
  {
    "path": "spec/util_spec.lua",
    "content": "local util = require(\"detour.util\")\n\ndescribe(\"detour util\", function()\n\tbefore_each(function()\n\t\tvim.g.detour_testing = true\n\t\tvim.cmd([[ \n      %bwipeout!\n      mapclear\n      nmapclear\n      vmapclear\n      xmapclear\n      smapclear\n      omapclear\n      mapclear\n      imapclear\n      lmapclear\n      cmapclear\n      tmapclear\n    ]])\n\t\tfor _, autocmd in ipairs(vim.api.nvim_get_autocmds({})) do\n\t\t\tif vim.startswith(autocmd.group_name, \"detour-\") then\n\t\t\t\tvim.api.nvim_del_autocmd(autocmd.id)\n\t\t\tend\n\t\tend\n\t\tvim.o.splitbelow = true\n\t\tvim.o.splitright = true\n\tend)\n\n\tit(\"pairs_by_keys iterates keys in order\", function()\n\t\tlocal t = { c = 3, a = 1, b = 2 }\n\t\tlocal keys = {}\n\t\tfor k in util.pairs_by_keys(t) do\n\t\t\ttable.insert(keys, k)\n\t\tend\n\t\tassert.same({ \"a\", \"b\", \"c\" }, keys)\n\tend)\n\n\tit(\"stringify maps digits to letters\", function()\n\t\tassert.same(\"bc\", util.stringify(12))\n\t\tassert.same(\"bcd\", util.stringify(123))\n\tend)\n\n\tit(\"base_at_screenpos finds base window by coordinates\", function()\n\t\tlocal tab = vim.api.nvim_get_current_tabpage()\n\t\t-- two columns layout\n\t\tlocal left = vim.api.nvim_get_current_win()\n\t\tvim.cmd.vsplit()\n\t\tlocal right = vim.api.nvim_get_current_win()\n\n\t\tlocal function inside(win)\n\t\t\tlocal top, _, leftx, _ = util.get_text_area_dimensions(win)\n\t\t\treturn top + 1, leftx + 1\n\t\tend\n\n\t\tlocal r, c = inside(left)\n\t\tassert.same(left, util.base_at_screenpos(tab, r, c))\n\n\t\tr, c = inside(right)\n\t\tassert.same(right, util.base_at_screenpos(tab, r, c))\n\tend)\nend)\n"
  },
  {
    "path": "spec/windowing_algorithm_spec.lua",
    "content": "local algo = require(\"detour.windowing_algorithm\")\nlocal util = require(\"detour.util\")\n\ndescribe(\"detour windowing_algorithm\", function()\n  before_each(function()\n    vim.g.detour_testing = true\n    vim.cmd([[ \n      %bwipeout!\n      mapclear\n      nmapclear\n      vmapclear\n      xmapclear\n      smapclear\n      omapclear\n      mapclear\n      imapclear\n      lmapclear\n      cmapclear\n      tmapclear\n    ]])\n    vim.api.nvim_clear_autocmds({})\n    vim.o.splitbelow = true\n    vim.o.splitright = true\n  end)\n\n  it(\"construct_nest returns inner rectangle inside parent float\", function()\n    -- create a parent float with known geometry and no border\n    local parent = vim.api.nvim_open_win(vim.api.nvim_get_current_buf(), true, {\n      relative = \"editor\",\n      row = 5,\n      col = 10,\n      width = 30,\n      height = 10,\n      border = \"none\",\n      zindex = 1,\n    })\n    assert.truthy(parent)\n\n    local top, bottom, left, right = util.get_text_area_dimensions(parent)\n    local expected_width = (right - left)\n    local expected_height = (bottom - top)\n    if expected_height >= 3 then\n      expected_height = expected_height - 2\n      top = top + 1\n    end\n    if expected_width >= 3 then\n      expected_width = expected_width - 2\n      left = left + 1\n    end\n\n    local opts = algo.construct_nest(parent, 2)\n    assert.same({\n      relative = \"editor\",\n      row = top,\n      col = left,\n      width = expected_width,\n      height = expected_height,\n      border = \"rounded\",\n      zindex = 2,\n    }, opts)\n  end)\nend)\n\n"
  }
]