Full Code of carbon-steel/detour.nvim for AI

main 204f61e64f87 cached
34 files
100.9 KB
28.3k tokens
1 requests
Download .txt
Repository: carbon-steel/detour.nvim
Branch: main
Commit: 204f61e64f87
Files: 34
Total size: 100.9 KB

Directory structure:
gitextract_mob2na4m/

├── .busted
├── .github/
│   └── workflows/
│       └── release.yml
├── .gitignore
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── detour.nvim-scm-1.rockspec
├── doc/
│   ├── detour.txt
│   └── tags
├── examples/
│   ├── help-float.md
│   └── telescope.md
├── lua/
│   └── detour/
│       ├── algorithm_doc.lua
│       ├── config.lua
│       ├── features.lua
│       ├── init.lua
│       ├── internal.lua
│       ├── movements.lua
│       ├── show_path_in_title.lua
│       ├── util.lua
│       └── windowing_algorithm.lua
├── plugin/
│   └── detour.lua
├── run-in-docker.sh
└── spec/
    ├── config_spec.lua
    ├── detour_auto_unreserve_spec.lua
    ├── detour_close_stack_spec.lua
    ├── detour_hide_reveal_spec.lua
    ├── detour_movements_spec.lua
    ├── detour_spec.lua
    ├── detour_uncover_spec.lua
    ├── internal_spec.lua
    ├── util_spec.lua
    └── windowing_algorithm_spec.lua

================================================
FILE CONTENTS
================================================

================================================
FILE: .busted
================================================
return {
  _all = {
    coverage = false,
    lpath = "lua/?.lua;lua/?/init.lua",
  },
  default = {
    verbose = true,
  },
  tests = {
    verbose = true,
  },
}


================================================
FILE: .github/workflows/release.yml
================================================
name: LuaRocks release
on:
  push:
    tags: # Will upload to luarocks.org when a tag is pushed
      - "*"
  pull_request: # Will test a local install without uploading to luarocks.org

jobs:
  luarocks-release:
    runs-on: ubuntu-latest
    name: LuaRocks upload
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: LuaRocks Upload
        uses: nvim-neorocks/luarocks-tag-release@v7
        env:
          LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}


================================================
FILE: .gitignore
================================================
# Compiled Lua sources
luac.out

# luarocks build files
*.src.rock
*.zip
*.tar.gz

# Object files
*.o
*.os
*.ko
*.obj
*.elf

# Precompiled Headers
*.gch
*.pch

# Libraries
*.lib
*.a
*.la
*.lo
*.def
*.exp

# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib

# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex

lua/.luarc.json

/luarocks
/lua_modules
/.luarocks

.nvimlog

# Temporary files for testing
image
images


================================================
FILE: CHANGELOG.md
================================================
# Changelog

## v2.0.0

### Added

* Features: Hide/reveal detours and uncover hidden base windows
* Implement `CloseCurrentStack`
* A default title to all detours based on buffer name
    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/6f7a718e1ea0d24daff16407b27e460e043ebf6f)
* This changelog
* Added help pages

### Fixed

* Keep cursor out of covered windows
    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/0c358da951addace23934db10df59cc609e81db4)
* Check window exists before updating its title
    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/6cd2b457e4a5502cdaaf510a3da66d2686d42cc9)
* Fix global statusline detection
    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/f452858a3bac44bdabb9f507ba219e3e0af4bc6c)
* Attempt to fix terminal rendering issue
    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/b5596b9baa61475fe5164142c7d8ca86d0cf3b37)
* Miscellaneous small fixes
    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/eaab89288dd14de8d7cd06a948589b8f439c12ad)
* Make updating title more responsive
    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/255fd9555d389d21a3bf790de47a2350b5607bf5)
* Guard title update autocmd
    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/0e206f5aacf9f65b2d92cc9098519a7ea3595536)
* Realigned terminal buffers after resize events
    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/a7935ce1283a141bcca09d6bdf07c9c1b537bbfb)
* Redid help detour example
    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/bf59c29a06b58cd0e9f53b04aad7646204af4479)
* Introduced nesting autocmds
    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/42a724730e2351057973e1231016b8918e161e4f)

### Changed

* Increase required Neovim version to `0.11`
    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/def7b8c2e7b930c1d9f807f4362e61fb8796f11e)
* Removed behavior that closes detour if its parents are closed.
    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/48d6e7031007f4ebda460b99beeecc50ef932bcc)
* Keep window sizing behavior consistent until very small sizes
    - [Commit](https://github.com/carbon-steel/detour.nvim/commit/39b19018711073edb0dd69a790e2ffdb4ebeb50c)


================================================
FILE: Dockerfile
================================================
FROM alpine:3.22

ENV LUA_MAJOR_VERSION 5.1
ENV LUA_MINOR_VERSION 5
ENV LUA_VERSION ${LUA_MAJOR_VERSION}.${LUA_MINOR_VERSION}

# Dependencies
RUN apk update && apk add --update make tar unzip gcc openssl-dev readline-dev curl libc-dev
RUN apk add wget # Needed due to https://github.com/luarocks/luarocks/issues/952

RUN curl -L http://www.lua.org/ftp/lua-${LUA_VERSION}.tar.gz | tar xzf -
WORKDIR /lua-$LUA_VERSION

# build lua
RUN make linux test
RUN make install

WORKDIR /

# lua env
ENV WITH_LUA /usr/local/
ENV LUA_LIB /usr/local/lib/lua
ENV LUA_INCLUDE /usr/local/include


RUN rm /lua-$LUA_VERSION -rf

ENV LUAROCKS_VERSION 3.9.2
ENV LUAROCKS_INSTALL luarocks-$LUAROCKS_VERSION
ENV TMP_LOC /tmp/luarocks

# Build Luarocks
RUN curl -OL \
    https://luarocks.org/releases/${LUAROCKS_INSTALL}.tar.gz

RUN tar xzf $LUAROCKS_INSTALL.tar.gz && \
    mv $LUAROCKS_INSTALL $TMP_LOC && \
    rm $LUAROCKS_INSTALL.tar.gz


WORKDIR $TMP_LOC

RUN ./configure \
  --with-lua=$WITH_LUA \
  --with-lua-include=$LUA_INCLUDE \
  --with-lua-lib=$LUA_LIB

RUN make build

RUN make install

WORKDIR /

RUN rm $TMP_LOC -rf

WORKDIR /mnt/luarocks

RUN apk add 'neovim=0.11.1-r1'
ENV BUSTED_VERSION 2.1.2-3
RUN luarocks install busted $BUSTED_VERSION


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023 Roger Kim

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
DOCKER_IMAGE = detour_tester

.PHONY : test

image: Dockerfile
	docker build -t ${DOCKER_IMAGE} .
	touch image

test: image
	docker run --volume="$(shell pwd):/mnt/luarocks:Z" ${DOCKER_IMAGE} /mnt/luarocks/run-in-docker.sh

.PHONY: help
# Ensure init.lua is processed first. Exclude internal helpers from docs.
HELP_LUA_ALL := $(wildcard lua/detour/*.lua)
# Space-separated list of files to exclude from lemmy-help
HELP_EXCLUDE := lua/detour/internal.lua \
                lua/detour/windowing_algorithm.lua \
                lua/detour/config.lua \
                lua/detour/util.lua \
                lua/detour/show_path_in_title.lua
HELP_LUA_SRCS := $(filter-out $(HELP_EXCLUDE),$(HELP_LUA_ALL))
# Ensure both init.lua and features.lua are first in order
HELP_LUA_HEAD := lua/detour/init.lua lua/detour/algorithm_doc.lua lua/detour/movements.lua
HELP_LUA_TAIL := $(filter-out $(HELP_LUA_HEAD),$(HELP_LUA_SRCS))
help:
	# Generate help docs via lemmy-help from Lua sources (with excludes)
	@mkdir -p doc
	lemmy-help $(HELP_LUA_HEAD) $(HELP_LUA_TAIL) > doc/detour.txt
	# Rebuild helptags (optional)
	@nvim --headless -c 'silent! helptags doc' -c 'q' || true


================================================
FILE: README.md
================================================
# detour.nvim

> 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.

<div dir="rtl">
J.R.R. Tolkien, The Lord of the Rings
</div>
</br></br>

`detour.nvim` provides commands to open floating windows that position and shape
themselves.

# Never lose your spot!📍🗺️

| What does detour.nvim do? | |
| :--: | :--: |
| `: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) |
| `: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) |
| 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) |
| You can nest detour popups | ![nest](https://github.com/carbon-steel/detour.nvim/assets/7697639/5fc3cad6-9acf-482d-97cb-c75788617cf8) |

Neovim's floating windows are a great utility to use in plugins and
functions, but they cannot be used manually. This is because
creating floats is not simple like calling `:split` or `:vsplit`.
`vim.api.nvim_open_win(...)` requires coordinates and dimensions to make
a float which is too tedious to do by hand.

Detour.nvim brings a single new feature to Neovim: **detour windows** (aka
detours). Detours are floating windows with the ease-of-use of splits.

Detours will make sure not to overlap each other unless when a detour is
nested within another.

# Example keymaps

`detour.nvim` is designed as a utility library for keymaps people can write on their own.

**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!

Here are a few basic examples...

### Open your Neovim config

```lua
vim.keymap.set("n", "<leader>e", function()
	-- Open detour
	if not require("detour").Detour() then
		return
	end
	vim.cmd.edit(vim.fn.stdpath("config")) -- open Neovim config directory
end)
```

[Screencast from 2025-09-11 07-37-45.webm](https://github.com/user-attachments/assets/9842dde2-3c42-4ade-aa27-35d23b45b42c)

### Jump to definition in detour

```lua
vim.keymap.set("n", "<leader>gd", function()
	-- Open detour with the same buffer
	if not require("detour").Detour() then
		return
	end
	vim.lsp.buf.definition() -- jump to definition
end)
```

[Screencast from 2025-09-13 18-57-09.webm](https://github.com/user-attachments/assets/5f71ace1-1392-4082-8daa-83be88669324)

### Wrap a TUI: top

You can wrap any TUI in a detour. Here is an example.

Run `top` in a detour:

```lua
vim.keymap.set("n", "<leader>p", function()
	local window_id = require("detour").Detour() -- open a detour
	if not window_id then
		return
	end

	vim.cmd.terminal("top") -- open a terminal buffer
	vim.bo.bufhidden = "delete" -- close the terminal when window closes
	vim.wo[window_id].signcolumn = "no" -- In Neovim 0.10, the signcolumn can push the TUI a bit out of window

	-- It's common for people to have `<Esc>` mapped to `<C-\><C-n>` for terminals.
	-- This can get in the way when interacting with TUIs.
	-- This maps the escape key back to itself (for this buffer) to fix this problem.
	vim.keymap.set("t", "<Esc>", "<Esc>", { buffer = true })

	vim.cmd.startinsert() -- go into insert mode

	vim.api.nvim_create_autocmd({ "TermClose" }, {
		buffer = vim.api.nvim_get_current_buf(),
		callback = function()
			-- This automated keypress skips for you the "[Process exited 0]" message
			-- that the embedded terminal shows.
			vim.api.nvim_feedkeys("i", "n", false)
		end,
	})
end)
```

||
| :--: |
| **Use keymap above -> Close window** |
![top](https://github.com/carbon-steel/detour.nvim/assets/7697639/49dd12ab-630b-4558-9486-fe82cc94882c)

# Installation

### Lazy.nvim

```lua
{ "carbon-steel/detour.nvim",
    config = function ()
        require("detour").setup({
            -- Put custom configuration here
        })
        vim.keymap.set('n', '<c-w><enter>', ":Detour<cr>")
        vim.keymap.set('n', '<c-w>.', ":DetourCurrentWindow<cr>")

        local detour_moves = require("detour.movements")
        -- NOTE: While using `detour_moves` is not required to use this
        -- plugin, it is strongly recommended as it makes window navigation
        -- much more intuitive.
        --
        -- The following keymaps are drop in replacements for Vim's regular
        -- window navigation commands. These replacements allows you to
        -- skip over windows covered by detours (which is a much more
        -- intuitive motion) but are otherwise the same as normal window
        -- navigation.
        --
        -- This is an example set of keymaps, but if you use other keys to
        -- navigate windows, changes these keymaps to suit your situation.
        vim.keymap.set({ "n", "t" }, "<C-j>", detour_moves.DetourWinCmdJ)
        vim.keymap.set({ "n", "t" }, "<C-w>j", detour_moves.DetourWinCmdJ)
        vim.keymap.set({ "n", "t" }, "<C-w><C-j>", detour_moves.DetourWinCmdJ)

        vim.keymap.set({ "n", "t" }, "<C-h>", detour_moves.DetourWinCmdH)
        vim.keymap.set({ "n", "t" }, "<C-w>h", detour_moves.DetourWinCmdH)
        vim.keymap.set({ "n", "t" }, "<C-w><C-h>", detour_moves.DetourWinCmdH)

        vim.keymap.set({ "n", "t" }, "<C-k>", detour_moves.DetourWinCmdK)
        vim.keymap.set({ "n", "t" }, "<C-w>k", detour_moves.DetourWinCmdK)
        vim.keymap.set({ "n", "t" }, "<C-w><C-k>", detour_moves.DetourWinCmdK)

        vim.keymap.set({ "n", "t" }, "<C-l>", detour_moves.DetourWinCmdL)
        vim.keymap.set({ "n", "t" }, "<C-w>l", detour_moves.DetourWinCmdL)
        vim.keymap.set({ "n", "t" }, "<C-w><C-l>", detour_moves.DetourWinCmdL)

        vim.keymap.set({ "n", "t" }, "<C-w>w", detour_moves.DetourWinCmdW)
        vim.keymap.set({ "n", "t" }, "<C-w><C-w>", detour_moves.DetourWinCmdW)

    end
},
```

# Options

| Option  | Description                                                                                 | Default value |
| --      | --                                                                                          | --            |
| `title` | "path" sets the path of the current buffer as the title of the float. "none" sets no title. | "path"        |

# Advanced

Using detours can be simple, but they also come with features for power users:

* `require("detour.features").UncoverWindow`: Update all detours to uncover a
  given regular window
* `require("detour.features").UncoverWindowWithMouse`: Same as above but select
  which window to uncover with the mouse
* `require("detour.features").HideAllDetours`/`RevealAllDetours`: Hide and
  reveal all detours so you can see the windows behind them
* `require("detour.features").CloseCurrentStack`: Closes the current detour
  along with all detours it is nested within

# Development

* Build help docs: run `make help` from the repo root.
    - Requires `lemmy-help` in your PATH ([repo](https://github.com/numToStr/lemmy-help/tree/master)).
    - Optional: Neovim available for generating `helptags` (target does not fail if absent).
* Run test: run `make test` from the repo root
    - Requires `docker`

# FAQ

> I want to convert detours to splits or tabs.

`<C-w>s` and `<C-w>v` can be used from within a popup to create splits. `<C-w>T` creates tabs.

> My LSP keeps moving my cursor to other windows.

If your LSP movements (ex: `go-to-definition`) are opening locations in other windows, make sure that `reuse_win` is set to `false`.

> My floating windows don't look good.

Some 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.

> My TUI is slightly wider than the floating window it's in.

This 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`.

```lua
vim.opt.signcolumn = "no"
```


================================================
FILE: detour.nvim-scm-1.rockspec
================================================
rockspec_format = '3.0'
package = 'detour.nvim'
version = 'scm-1'

test_dependencies = {
  'lua >= 5.1',
}

source = {
  url = 'git://github.com/carbon-steel/' .. package,
}

build = {
  type = 'builtin',
}


================================================
FILE: doc/detour.txt
================================================
==============================================================================
                                                                        *detour*

                                                                   *detour-into*
Neovim/Vim's floating windows are a great utility to use in plugins and
functions, but they cannot be used manually as splits are. This is because
creating floats is not simple like calling `:split` or `:vsplit`.
`vim.api.nvim_open_win(...)` requires coordinates and dimensions to make
a float which is too tedious to do by hand.

Detour.nvim brings a single new feature to Neovim: detour windows (aka
detours). Detours are floating windows with the ease-of-use of splits.

They dynamically shape themselves to cover as much of a given area as possible.
They can cover:
* the whole screen (`require("detour").Detour()`)
* the current window (`require("detour").DetourCurrentWindow()`)
* the current detour (both of the above functions would work)

Detours will make sure not to overlap each other unless when a detour is
nested within another.

You will find that there are many cases where using a large floating window is
preferable to creating a smaller split window. On top of that, the nesting
behavior of detours allows you to take a long "detour" into other files/locations
without losing your place in your regular windows. Take a detour, look at
other locations, close the detour, and find your original windows as they
were when you left.

detour.Detour()                                                  *detour.Detour*
    Open a new detour window

    * If this is called from a non-detour window, the largest possible detour
    window will be opened that does not overlap with any other detours.
    * If this is called from a detour window, a detour will be opened nested
    within just the current detour.

    There are cases where there is no space for a new detour window and
    this function call will do nothing.

    Returns: ~
        (integer|nil)  returns detour's window id if successfully
                       created, nil otherwise
                       @nodiscard

    Usage: ~
>lua

        --- local window_id = require("detour").Detour()
        --- if not window_id then
        ---     -- New detour could not be made so stop execution
        ---     return
        --- end
        ---
        --- -- New detour window is open and cursor is moved to it.
<


detour.DetourCurrentWindow()                        *detour.DetourCurrentWindow*
    Open a detour popup covering only the current window.

    There are cases where there is no space for a new detour window and
    this function call will do nothing.

    Returns: ~
        (integer|nil)  returns detour's window id if successfully
                       created, nil otherwise
                       @nodiscard

    Usage: ~
>lua

        --- local window_id = require("detour").DetourCurrentWindow()
        --- if not window_id then
        --- 	-- New detour could not be made so stop execution
        --- 	return
        --- end
        ---
        --- -- New detour window is open and cursor is moved to it.
<


==============================================================================
                                                              *detour.algorithm*

                                                              *detour-algorithm*
Detour's windowing algorithm computes the largest rectangle where a
floating window (detour) can be placed without overlapping windows that must
remain visible.

Overview

 Each detour has a list of "reserved" windows that it is allowed to cover.

 When creating or resizing a detour, the algorithm will find the largest
 rectangular area where a floating window can go that covers only reserved
 windows. That rectangle will be the position and dimensions of the detour.

 `Detour()` creates a detour `d` where all windows that are not currently
 reserved by an existing detour are reserved by `d`.

 `DetourCurrentWindow()` creates a detour `d` where only the current window
 is reserved by `d`.

Resizing

 Whenever windows open, close, or get resized, detours will recalculate the
 largest area they can fill and dynamically reshape themselves. This allows
 them to make room for new windows or to expand to take space that has been
 freed up.

==============================================================================
                                                              *detour.movements*

Detour.nvim breaks window navigation commands such as
`<C-w>w`, `<C-w>j`, or `vim.cmd.wincmd("h")`. Instead of
using those, the user MUST use the following window navigation
commands that this plugin provides that implements moving
between windows while skipping over windows covered by detours.

NOTE: Regular window movements such as `<C-w>w`, `<C-w>j`,
`vim.cmd.wincmd("h")` should still work in automated
scripts/functions. Still, you may find it more useful to use detour's
"detour-aware" movement functions in your scripts/functions as well.

                                                              *detour-movements*
movements._safe_state_handler()                  *movements._safe_state_handler*
    DO NOT USE. FOR TESTING ONLY.


movements.DetourWinCmdL()                              *movements.DetourWinCmdL*
     Switch to a window to the right. Skip over any non-floating windows
     covered by a detour.

    Returns: ~
        (nil)

    Usage: ~
>lua

        --- local detour_moves = require("detour.movements")
        --- vim.keymap.set({ "n", "t" }, "<C-l>", detour_moves.DetourWinCmdL)
        --- vim.keymap.set({ "n", "t" }, "<C-w>l", detour_moves.DetourWinCmdL)
        --- vim.keymap.set({ "n", "t" }, "<C-w><C-l>", detour_moves.DetourWinCmdL)
        ---
<


movements.DetourWinCmdH()                              *movements.DetourWinCmdH*
     Switch to a window to the left. Skip over any non-floating windows
     covered by a detour.

    Returns: ~
        (nil)

    Usage: ~
>lua

        --- local detour_moves = require("detour.movements")
        --- vim.keymap.set({ "n", "t" }, "<C-h>", detour_moves.DetourWinCmdH)
        --- vim.keymap.set({ "n", "t" }, "<C-w>h", detour_moves.DetourWinCmdH)
        --- vim.keymap.set({ "n", "t" }, "<C-w><C-h>", detour_moves.DetourWinCmdH)
        ---
<


movements.DetourWinCmdJ()                              *movements.DetourWinCmdJ*
     Switch to a window below. Skip over any non-floating windows
     covered by a detour.

    Returns: ~
        (nil)

    Usage: ~
>lua

        --- local detour_moves = require("detour.movements")
        --- vim.keymap.set({ "n", "t" }, "<C-j>", detour_moves.DetourWinCmdJ)
        --- vim.keymap.set({ "n", "t" }, "<C-w>j", detour_moves.DetourWinCmdJ)
        --- vim.keymap.set({ "n", "t" }, "<C-w><C-j>", detour_moves.DetourWinCmdJ)
        ---
<


movements.DetourWinCmdK()                              *movements.DetourWinCmdK*
     Switch to a window above. Skip over any non-floating windows
     covered by a detour.

    Returns: ~
        (nil)

    Usage: ~
>lua

        --- local detour_moves = require("detour.movements")
        --- vim.keymap.set({ "n", "t" }, "<C-k>", detour_moves.DetourWinCmdK)
        --- vim.keymap.set({ "n", "t" }, "<C-w>k", detour_moves.DetourWinCmdK)
        --- vim.keymap.set({ "n", "t" }, "<C-w><C-k>", detour_moves.DetourWinCmdK)
        ---
<


movements.DetourWinCmdW()                              *movements.DetourWinCmdW*
     Switch windows in a cycle. Skip over any non-floating windows covered
     by a detour.

    Returns: ~
        (nil)

    Usage: ~
>lua

        --- local detour_moves = require("detour.movements")
        --- vim.keymap.set({ "n", "t" }, "<C-w>", detour_moves.DetourWinCmdW)
        --- vim.keymap.set({ "n", "t" }, "<C-w>w", detour_moves.DetourWinCmdW)
        --- vim.keymap.set({ "n", "t" }, "<C-w><C-w>", detour_moves.DetourWinCmdW)
        ---
<


==============================================================================
                                                               *detour.features*

 Optional detour.nvim features.

                                                               *detour-features*
features.ShowPathInTitle({popup_id})                  *features.ShowPathInTitle*
    Show the buffer path in the given popup's title and keep it updated.

    Parameters: ~
        {popup_id}  (integer)

    Returns: ~
        (nil)


features.CloseOnLeave({popup_id})                        *features.CloseOnLeave*
    Close the popup when focus leaves to a non-floating window.

    Parameters: ~
        {popup_id}  (integer)

    Returns: ~
        (nil)


features.UncoverWindow({window})                        *features.UncoverWindow*
    Prevent detours from covering the provided window.

    Parameters: ~
        {window}  (integer)

    Returns: ~
        (boolean)


features.UncoverWindowWithMouse()              *features.UncoverWindowWithMouse*
    Prompt to click a window and mark it as uncovered by detours.

    Returns: ~
        (nil)


features.HideAllDetours()                              *features.HideAllDetours*
    Temporarily hide all detours in current tabpage.

    Returns: ~
        (nil)


features.RevealAllDetours()                          *features.RevealAllDetours*
    Reveal all detours previously hidden in current tabpage.

    Returns: ~
        (nil)


features.CloseCurrentStack()                        *features.CloseCurrentStack*
     Closes current detour along with all detours that it covers and covers it.
     When not inside a detour, this function is a no-op.

    Returns: ~
        (boolean)  true if close operation succeeded and false otherwise
                   @nodiscard


vim:tw=78:ts=8:noet:ft=help:norl:


================================================
FILE: doc/tags
================================================
detour	detour.txt	/*detour*
detour-algorithm	detour.txt	/*detour-algorithm*
detour-features	detour.txt	/*detour-features*
detour-into	detour.txt	/*detour-into*
detour-movements	detour.txt	/*detour-movements*
detour.Detour	detour.txt	/*detour.Detour*
detour.DetourCurrentWindow	detour.txt	/*detour.DetourCurrentWindow*
detour.algorithm	detour.txt	/*detour.algorithm*
detour.features	detour.txt	/*detour.features*
detour.movements	detour.txt	/*detour.movements*
features.CloseCurrentStack	detour.txt	/*features.CloseCurrentStack*
features.CloseOnLeave	detour.txt	/*features.CloseOnLeave*
features.HideAllDetours	detour.txt	/*features.HideAllDetours*
features.RevealAllDetours	detour.txt	/*features.RevealAllDetours*
features.ShowPathInTitle	detour.txt	/*features.ShowPathInTitle*
features.UncoverWindow	detour.txt	/*features.UncoverWindow*
features.UncoverWindowWithMouse	detour.txt	/*features.UncoverWindowWithMouse*
movements.DetourWinCmdH	detour.txt	/*movements.DetourWinCmdH*
movements.DetourWinCmdJ	detour.txt	/*movements.DetourWinCmdJ*
movements.DetourWinCmdK	detour.txt	/*movements.DetourWinCmdK*
movements.DetourWinCmdL	detour.txt	/*movements.DetourWinCmdL*
movements.DetourWinCmdW	detour.txt	/*movements.DetourWinCmdW*
movements._safe_state_handler	detour.txt	/*movements._safe_state_handler*


================================================
FILE: examples/help-float.md
================================================
# Display help files in a Detour window

```lua
vim.keymap.set("n", "<A-h>", function()
	local popup_id = require("detour").Detour()
	if popup_id then
		require("telescope.builtin").live_grep({
			cwd = vim.fs.joinpath(vim.env.VIMRUNTIME, "doc"),
		})
	else
		local keys = vim.api.nvim_replace_termcodes(":h ", true, true, true)
		vim.api.nvim_feedkeys(keys, "n", true)
	end
end)
```


================================================
FILE: examples/telescope.md
================================================
# Telescope keymaps
Here are examples of useful keymaps where you use detour popups together with the [telescope plugin](https://github.com/nvim-telescope/telescope.nvim).

### Terminal selection
Select an existing terminal to open in a popup. If none exist, open a new one. 
```lua
vim.keymap.set('n', '<leader>t', function()
    local terminal_buffer_found = false
    -- Check if we there are any existing terminal buffers.
    for _, buf in ipairs(vim.api.nvim_list_bufs()) do -- iterate through all buffers
        if vim.api.nvim_buf_is_loaded(buf) then       -- only check loaded buffers
            if vim.api.nvim_buf_get_option(buf, "buftype") == "terminal" then
                terminal_buffer_found = true
            end
        end
    end

    require('detour').Detour()                      -- Open a detour popup
    if terminal_buffer_found then
        require('telescope.builtin').buffers({})    -- Open telescope prompt
        vim.api.nvim_feedkeys("term://", "n", true) -- populate prompt with "term://"
    else
        -- [OPTIONAL] Set the new window's current working directory to the directory of current file.
        -- You can remove this line if you would prefer to open terminals from the
        -- existing working directory.
        vim.cmd.lcd(vim.fn.expand("%:p:h"))
        -- Since there are no existing terminal buffers, open a new one.
        vim.cmd.terminal()
        vim.cmd.startinsert()
    end
end)
```


================================================
FILE: lua/detour/algorithm_doc.lua
================================================
---@mod detour.algorithm
---@tag detour-algorithm
---@brief [[
---Detour's windowing algorithm computes the largest rectangle where a
---floating window (detour) can be placed without overlapping windows that must
---remain visible.
---
---Overview
---
--- Each detour has a list of "reserved" windows that it is allowed to cover.
---
--- When creating or resizing a detour, the algorithm will find the largest
--- rectangular area where a floating window can go that covers only reserved
--- windows. That rectangle will be the position and dimensions of the detour.
---
--- `Detour()` creates a detour `d` where all windows that are not currently
--- reserved by an existing detour are reserved by `d`.
---
--- `DetourCurrentWindow()` creates a detour `d` where only the current window
--- is reserved by `d`.
---
---Resizing
---
--- Whenever windows open, close, or get resized, detours will recalculate the
--- largest area they can fill and dynamically reshape themselves. This allows
--- them to make room for new windows or to expand to take space that has been
--- freed up.
---@brief ]]
local algorithm_doc = {}

--- Internal anchor to ensure module docs render. Do not use.
---@private
function algorithm_doc._doc_anchor() end

return algorithm_doc


================================================
FILE: lua/detour/config.lua
================================================
---@mod detour.config
---User configuration and defaults for detour.nvim.

---@class detour.config.Options
---@field title "none"|"path"

local config = {}

---@type detour.config.Options
local defaults = {
	title = "path",
}

---@type detour.config.Options
config.options = {}

---Setup detour.nvim configuration.
---@param args? detour.config.Options
config.setup = function(args)
	args = args or {}

	local new_options =
		vim.tbl_deep_extend("force", defaults, config.options, args or {})

	if not vim.tbl_contains({ "none", "path" }, new_options.title) then
		vim.api.nvim_echo({
			{
				'"'
					.. tostring(new_options.title)
					.. '" is an invalid value for title. Not changing detour configs.',
			},
		}, true, { err = true })
		return
	end

	config.options = new_options
end

config.setup()

return config


================================================
FILE: lua/detour/features.lua
================================================
---@mod detour.features
---@brief [[
--- Optional detour.nvim features.
---@brief ]]
---@tag detour-features

local features = {}

local util = require("detour.util")
local internal = require("detour.internal")

---Update the detour window title to the current buffer path relative to cwd.
---@param window_id integer
---@return nil
local function update_title(window_id)
	local tabwin = vim.fn.win_id2tabwin(window_id)
	local tabnr, winnr = unpack(tabwin)
	if tabnr == 0 and winnr == 0 then
		return
	end
	local buffer_id = vim.api.nvim_win_get_buf(window_id)
	local path = vim.api.nvim_buf_get_name(buffer_id)
	local home = vim.fn.getcwd(winnr, tabnr)
	local title = vim.fn.fnamemodify(path, ":.")
	if title:sub(1, #home) == home then
		title = title:sub(#home + 1)
	end
	vim.api.nvim_win_set_config(
		window_id,
		vim.tbl_extend(
			"force",
			vim.api.nvim_win_get_config(window_id),
			{ title = title }
		)
	)
end

---Show the buffer path in the given popup's title and keep it updated.
---@param popup_id integer
---@return nil
function features.ShowPathInTitle(popup_id)
	require("detour.show_path_in_title")

	if
		next(vim.api.nvim_get_autocmds({
			pattern = "DetourUpdateTitle" .. util.stringify(popup_id),
			group = internal.construct_augroup_name(popup_id),
		})) ~= nil
	then
		-- ShowPathInTitle already called for this popup.
		return
	end

	update_title(popup_id)

	vim.api.nvim_create_autocmd({ "User" }, {
		pattern = "DetourUpdateTitle" .. util.stringify(popup_id),
		group = internal.construct_augroup_name(popup_id),
		callback = function()
			if not util.is_open(popup_id) then
				return true
			end
			update_title(popup_id)
		end,
	})
end

---Close the popup when focus leaves to a non-floating window.
---@param popup_id integer
---@return nil
function features.CloseOnLeave(popup_id)
	-- This autocmd will close the created detour popup when you focus on a different window.
	vim.api.nvim_create_autocmd({ "WinEnter" }, {
		group = internal.construct_augroup_name(popup_id),
		callback = function()
			local curr_window = vim.api.nvim_get_current_win()
			-- Skip cases where we are entering popups (eg, menus, nested popups, the detour popup itself).
			if vim.api.nvim_win_get_config(curr_window).relative ~= "" then
				return
			end

			-- Check to make sure the popup has not already been closed
			if util.is_open(popup_id) then
				vim.api.nvim_win_close(popup_id, false)
			end
		end,
		nested = true,
	})
end

---Prevent detours from covering the provided window.
---@param window integer
---@return boolean
function features.UncoverWindow(window)
	local ok = internal.unreserve_window(window)
	if ok then
		vim.api.nvim_exec_autocmds("VimResized", {})
	end
	return ok
end

---Prompt to click a window and mark it as uncovered by detours.
---@return nil
function features.UncoverWindowWithMouse()
	local prev_mouse = vim.o.mouse
	if not prev_mouse:match("a") then
		vim.o.mouse = "a"
	end
	vim.api.nvim_echo(
		{ { "Click a window (Press any key to cancel)…", "Question" } },
		false,
		{}
	)

	features.HideAllDetours()
	vim.g.detour_temp_uncover = 0
	vim.cmd([[
        let c = getchar()
        if c == "\<LeftMouse>" && v:mouse_win > 0
            let g:detour_temp_uncover=1
        endif
    ]])

	features.RevealAllDetours()

	vim.o.mouse = prev_mouse
	vim.cmd("echo '' | redraw!") -- clear the prompt

	if vim.g.detour_temp_uncover == 0 then
		vim.o.mouse = prev_mouse
		return
	end

	local m = vim.fn.getmousepos()
	local winid = util.base_at_screenpos(
		vim.api.nvim_get_current_tabpage(),
		m.screenrow,
		m.screencol
	)

	if winid then
		features.UncoverWindow(winid)
	end
end

---Temporarily hide all detours in current tabpage.
---@return nil
function features.HideAllDetours()
	for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
		if internal.is_detour(win) then
			vim.api.nvim_win_set_config(win, { hide = true })
		end
	end
	vim.cmd("redraw!")
end

---Reveal all detours previously hidden in current tabpage.
---@return nil
function features.RevealAllDetours()
	for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
		if internal.is_detour(win) then
			vim.api.nvim_win_set_config(win, { hide = false })
		end
	end
	vim.cmd("redraw!")
end

--- Closes current detour along with all detours that it covers and covers it.
--- When not inside a detour, this function is a no-op.
---@return boolean success true if close operation succeeded and false otherwise
---@nodiscard
function features.CloseCurrentStack()
	internal.garbage_collect()

	local current_window = vim.api.nvim_get_current_win()
	local covered = internal.get_reserved_windows(current_window)
	while covered do
		local parent = covered[1]
		vim.api.nvim_win_close(current_window, false)
		if vim.api.nvim_get_current_win() == current_window then
			-- Close operation failed
			return false
		end
		vim.fn.win_gotoid(parent)
		current_window = parent
		covered = internal.get_reserved_windows(vim.api.nvim_get_current_win())
	end

	return true
end

return features


================================================
FILE: lua/detour/init.lua
================================================
---@mod detour
---@tag detour-into
---@brief [[
---Neovim/Vim's floating windows are a great utility to use in plugins and
---functions, but they cannot be used manually as splits are. This is because
---creating floats is not simple like calling `:split` or `:vsplit`.
---`vim.api.nvim_open_win(...)` requires coordinates and dimensions to make
---a float which is too tedious to do by hand.
---
---Detour.nvim brings a single new feature to Neovim: detour windows (aka
---detours). Detours are floating windows with the ease-of-use of splits.
---
---They dynamically shape themselves to cover as much of a given area as possible.
---They can cover:
---* the whole screen (`require("detour").Detour()`)
---* the current window (`require("detour").DetourCurrentWindow()`)
---* the current detour (both of the above functions would work)
---
---Detours will make sure not to overlap each other unless when a detour is
---nested within another.
---
---You will find that there are many cases where using a large floating window is
---preferable to creating a smaller split window. On top of that, the nesting
---behavior of detours allows you to take a long "detour" into other files/locations
---without losing your place in your regular windows. Take a detour, look at
---other locations, close the detour, and find your original windows as they
---were when you left.
---@brief ]]

local detour = {}

local util = require("detour.util")
local internal = require("detour.internal")
local algo = require("detour.windowing_algorithm")
local settings = require("detour.config")

---Resize an existing popup and update covered windows' focusability.
---@param window_id integer
---@param new_window_opts table
local function resize_popup(window_id, new_window_opts)
	local current_window_opts = vim.api.nvim_win_get_config(window_id)
	vim.api.nvim_win_set_config(
		window_id,
		vim.tbl_extend("force", current_window_opts, new_window_opts)
	)

	-- Not sure why this loop is necessary.
	for _, covered_window in
		ipairs(internal.get_reserved_windows(window_id) or {})
	do
		if util.is_floating(covered_window) then
			vim.api.nvim_win_set_config(
				covered_window,
				vim.tbl_extend(
					"force",
					vim.api.nvim_win_get_config(covered_window),
					{ focusable = not util.overlap(covered_window, window_id) }
				)
			)
		end
	end

	-- Sometimes, resizing terminal buffers can end up scrolling the terminal UI
	-- horizontally so that you only see a portion of the terminal UI. This code
	-- scrolls the terminal UI so that the window keeps showing the leftmost
	-- column
	--
	-- Condition on terminal mode (instead of checking whether buffer is
	-- terminal-type) just in case user was intentionally looking at a specific
	-- location in visual or normal mode.
	if vim.fn.mode() == "t" then
		local row = vim.api.nvim_win_get_position(window_id)[1]
		vim.api.nvim_win_set_cursor(window_id, { row, 0 })
	end

	-- Fully complete resizing before propogating event.
	vim.api.nvim_exec_autocmds("User", {
		pattern = "DetourPopupResized" .. util.stringify(window_id),
	})
end

---Create a nested popup above the current floating window.
---@return integer|nil popup_id
local function popup_above_float()
	local parent = vim.api.nvim_get_current_win()

	if vim.tbl_contains(internal.list_reserved_windows(), parent) then
		vim.api.nvim_echo({
			{
				"[detour.nvim] This popup already has a child nested inside it: "
					.. parent,
			},
		}, true, { err = true })
		return nil
	end

	local parent_zindex = util.get_maybe_zindex(parent) or 0
	local window_opts = algo.construct_nest(parent, parent_zindex + 1)

	local child =
		vim.api.nvim_open_win(vim.api.nvim_win_get_buf(0), true, window_opts)
	if not internal.record_popup(child, { parent }) then
		vim.api.nvim_win_close(child, true)
		return nil
	end

	local augroup_id =
		vim.api.nvim_create_augroup(internal.construct_augroup_name(child), {})
	vim.api.nvim_create_autocmd({ "User" }, {
		pattern = "DetourPopupResized" .. util.stringify(parent),
		group = augroup_id,
		callback = function()
			if not util.is_open(child) then
				internal.teardown_detour(child)
				return
			end

			if util.is_open(parent) then
				resize_popup(child, algo.construct_nest(parent))
			end
		end,
	})
	vim.api.nvim_create_autocmd({ "WinClosed" }, {
		group = augroup_id,
		pattern = tostring(child),
		callback = function()
			internal.teardown_detour(child)
			if util.is_open(parent) then
				vim.fn.win_gotoid(parent)
			end
		end,
		nested = true, -- Trigger all the autocmds for entering a new window
	})

	if settings.options.title == "path" then
		require("detour.features").ShowPathInTitle(child)
	end

	-- We're running this to make sure initializing popups runs the same code
	-- path as updating popups
	-- We make sure to do this after all state and autocmds are set.
	vim.api.nvim_exec_autocmds("User", {
		pattern = "DetourPopupResized" .. util.stringify(parent),
	})
	return child
end

---Create a base popup covering the given windows (or all non-floating windows).
---@param bufnr integer
---@param reserve_windows integer[]?
---@return integer|nil popup_id
local function popup(bufnr, reserve_windows)
	local tab_id = vim.api.nvim_get_current_tabpage()
	reserve_windows = reserve_windows
		or vim.tbl_filter(function(window)
			return not (
				util.is_floating(window)
				or vim.tbl_contains(internal.list_reserved_windows(), window)
			)
		end, vim.api.nvim_tabpage_list_wins(tab_id))

	if #reserve_windows == 0 then
		vim.api.nvim_echo(
			{ { "[detour.nvim] No windows provided in coverable_windows." } },
			true,
			{ err = true }
		)
		return nil
	end

	for _, window in ipairs(reserve_windows) do
		if util.is_floating(window) then
			vim.api.nvim_echo({
				{
					"[detour.nvim] No floating windows allowed in base (ie, non-nested) popup "
						.. window,
				},
			}, true, { err = true })
			return nil
		end

		if vim.tbl_contains(internal.list_reserved_windows(), window) then
			vim.api.nvim_echo({
				{
					"[detour.nvim] This window is already reserved by another detour: "
						.. window,
				},
			}, true, { err = true })
			return nil
		end
	end

	local window_opts = algo.construct_window_opts(reserve_windows, tab_id)
	if window_opts == nil then
		return nil
	end

	local popup_id = vim.api.nvim_open_win(bufnr, true, window_opts)
	if not internal.record_popup(popup_id, reserve_windows) then
		vim.api.nvim_win_close(popup_id, true)
		return nil
	end

	local augroup_id = vim.api.nvim_create_augroup(
		internal.construct_augroup_name(popup_id),
		{}
	)

	vim.api.nvim_create_autocmd({ "WinResized" }, {
		group = augroup_id,
		callback = function(event)
			local reserved = internal.get_reserved_windows(popup_id)
			if reserved == nil then
				internal.teardown_detour(popup_id)
				return
			end
			-- WinResized populates vim.v.event.windows but VimResized does not
			-- so we default to listing all windows.
			-- Use event.data.windows for tests.
			local windows = vim.tbl_get(vim.v["event"], "windows") == nil
					and event.data.windows
				or vim.v["event"]["windows"]
			local changed_window =
				assert(windows[1], "no windows listed in WinResized event")
			local changed_tab = vim.api.nvim_win_get_tabpage(changed_window)
			if tab_id == changed_tab then
				local new_window_opts =
					algo.construct_window_opts(reserved, tab_id)
				if new_window_opts then
					resize_popup(popup_id, new_window_opts)
				end
			end
		end,
	})

	vim.api.nvim_create_autocmd({ "VimResized" }, {
		group = augroup_id,
		callback = function()
			internal.garbage_collect()
			local reserved = internal.get_reserved_windows(popup_id)
			if reserved == nil then
				internal.teardown_detour(popup_id)
				return
			end

			local new_window_opts = algo.construct_window_opts(reserved, tab_id)
			-- If there is an issue that prevents a valid configuration for the
			-- detour, just leave it for the user to manually clean up.
			if new_window_opts then
				resize_popup(popup_id, new_window_opts)
			end
		end,
	})

	vim.api.nvim_create_autocmd({ "WinClosed" }, {
		group = augroup_id,
		pattern = "" .. popup_id,
		callback = function()
			local reserved = internal.get_reserved_windows(popup_id)
			internal.teardown_detour(popup_id)
			for _, base in ipairs(reserved) do
				if
					vim.tbl_contains(
						vim.api.nvim_tabpage_list_wins(tab_id),
						base
					)
				then
					vim.fn.win_gotoid(base)
					return
				end
			end
		end,
		nested = true, -- Trigger all the autocmds for entering a new window
	})

	if settings.options.title == "path" then
		require("detour.features").ShowPathInTitle(popup_id)
	end

	-- We're running this to make sure initializing popups runs the same code
	-- path as updating popups. We make sure to do this after all state and
	-- autocmds are set.
	vim.api.nvim_exec_autocmds("VimResized", {})

	return popup_id
end

---Open a new detour window
---
---* If this is called from a non-detour window, the largest possible detour
---window will be opened that does not overlap with any other detours.
---* If this is called from a detour window, a detour will be opened nested
---within just the current detour.
---
---There are cases where there is no space for a new detour window and
---this function call will do nothing.
---@return integer|nil popup_id returns detour's window id if successfully
---created, nil otherwise
---@nodiscard
---@usage `
--- local window_id = require("detour").Detour()
--- if not window_id then
---     -- New detour could not be made so stop execution
---     return
--- end
---
--- -- New detour window is open and cursor is moved to it.`
detour.Detour = function()
	internal.garbage_collect()
	if util.is_floating(vim.api.nvim_get_current_win()) then
		return popup_above_float()
	end

	return popup(vim.api.nvim_get_current_buf())
end

---Open a detour popup covering only the current window.
---
---There are cases where there is no space for a new detour window and
---this function call will do nothing.
---@return integer|nil popup_id returns detour's window id if successfully
---created, nil otherwise
---@nodiscard
---@usage `
--- local window_id = require("detour").DetourCurrentWindow()
--- if not window_id then
--- 	-- New detour could not be made so stop execution
--- 	return
--- end
---
--- -- New detour window is open and cursor is moved to it.`
detour.DetourCurrentWindow = function()
	internal.garbage_collect()

	if util.is_floating(vim.api.nvim_get_current_win()) then
		return popup_above_float()
	end

	return popup(
		vim.api.nvim_get_current_buf(),
		{ vim.api.nvim_get_current_win() }
	)
end

detour.setup = require("detour.config").setup

return detour


================================================
FILE: lua/detour/internal.lua
================================================
-- DO NOT DEPEND ON THIS FILE!
-- This is an "internal" file and can have breaking changes without warning.

---@mod detour.internal
---Internal implementation details. Not part of the public API.

local internal = {}

---@class detour.internal
---@field construct_augroup_name fun(window_id: integer): string
---@field teardown_detour fun(window_id: integer)
---@field record_popup fun(popup_id: integer, coverable_windows: integer[]): boolean
---@field list_popups fun(): integer[]
---@field list_reserved_windows fun(): integer[]
---@field garbage_collect fun()
---@field is_detour fun(window: integer): boolean
---@field get_reserved_windows fun(popup_id: integer): integer[]|nil
---@field unreserve_window fun(window: integer): boolean

---@type table<integer, integer[]>
local popup_to_reserved_windows = {}

---@param window_id integer
---@return string
function internal.construct_augroup_name(window_id)
	return "detour-" .. window_id
end

-- Needs to be idempotent
---@param window_id integer
function internal.teardown_detour(window_id)
	-- Be tolerant if the augroup was already removed by another path.
	pcall(
		vim.api.nvim_del_augroup_by_name,
		internal.construct_augroup_name(window_id)
	)
	for _, covered_window in
		ipairs(internal.get_reserved_windows(window_id) or {})
	do
		if vim.api.nvim_win_get_config(covered_window).relative ~= "" then
			vim.api.nvim_win_set_config(
				covered_window,
				vim.tbl_extend(
					"force",
					vim.api.nvim_win_get_config(covered_window),
					{ focusable = true }
				)
			)
		end
	end
	popup_to_reserved_windows[window_id] = nil
end

function internal.is_detour(window)
	return popup_to_reserved_windows[window] ~= nil
end

---@param popup_id integer
---@return integer[]|nil
function internal.get_reserved_windows(popup_id)
	if popup_to_reserved_windows[popup_id] == nil then
		return nil
	end

	-- Clean up any windows that have already been closed
	popup_to_reserved_windows[popup_id] = vim.tbl_filter(function(window_id)
		return vim.tbl_contains(vim.api.nvim_list_wins(), window_id)
	end, popup_to_reserved_windows[popup_id])

	return popup_to_reserved_windows[popup_id]
end

---@param popup_id integer
---@param coverable_windows integer[]
---@return boolean
function internal.record_popup(popup_id, coverable_windows)
	local open_windows = vim.api.nvim_list_wins()
	coverable_windows = vim.tbl_filter(function(window_id)
		return vim.tbl_contains(open_windows, window_id)
	end, coverable_windows)

	if #coverable_windows == 0 then
		vim.api.nvim_echo({
			{
				"[detour.nvim] You must provide at least one valid (open) coverable window.",
			},
		}, true, { err = true })
		return false
	end
	popup_to_reserved_windows[popup_id] = coverable_windows
	return true
end

---@return integer[]
function internal.list_popups()
	return vim.tbl_keys(popup_to_reserved_windows)
end

---@return integer[]
function internal.list_reserved_windows()
	local windows = vim.api.nvim_list_wins()
	return vim.iter(vim.tbl_values(popup_to_reserved_windows))
		:flatten()
		:filter(function(w)
			return vim.tbl_contains(windows, w) -- make sure window is still open
		end)
		:totable()
end

---@param window integer
---@return boolean
function internal.unreserve_window(window)
	internal.garbage_collect()
	local changed = false
	local copy = vim.tbl_extend("force", popup_to_reserved_windows, {})
	for popup, reserved_windows in pairs(popup_to_reserved_windows) do
		copy[popup] = vim.iter(reserved_windows)
			:filter(function(reserved)
				if reserved ~= window then
					return true
				end
				changed = true
				return false
			end)
			:totable()
		if #copy[popup] == 0 then
			vim.api.nvim_echo({
				{
					"[detour.nvim] A detour must have at least one window to float over. Detour id: "
						.. popup,
				},
			}, true, { err = true })
			return false
		end
	end
	popup_to_reserved_windows = copy
	return changed
end

-- Neovim autocmd events are quite nuanced:
-- 1. Autocmds do not trigger autocmd events by default (you need to set `nested
--    = true` to do that).
-- 2. WinClosed autocmds do not trigger WinClosed events even if `nested = true`.
-- 3. Even with `nested = true`, there is a limit to how many nested events
--    Neovim will trigger (max depth is 10).
-- Hence, there are possible cases where popup detours will be closed by the
-- user's autocmds without triggering a WinClosed event. To address this, we
-- must make sure to update the plugin's state before executing each user
-- command. Also, we must double check what windows are still open during this
-- plugin's autocmd callbacks.
function internal.garbage_collect()
	for _, popup_id in ipairs(internal.list_popups()) do
		if not vim.tbl_contains(vim.api.nvim_list_wins(), popup_id) then
			internal.teardown_detour(popup_id)
		end
	end
end

assert(
	vim.fn.timer_start(
		300,
		vim.schedule_wrap(internal.garbage_collect),
		{ ["repeat"] = -1 }
	) ~= -1,
	"[detour.nvim] Failed to create garbage_collect timer."
)

local group = vim.api.nvim_create_augroup("detour_internal", {})

local just_entered_window = false
vim.api.nvim_create_autocmd({ "WinEnter" }, {
	group = group,
	callback = function()
		just_entered_window = true
	end,
})

-- If the user interacts with a window, we should prevent detours from covering
-- it.
vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, {
	group = group,
	callback = function()
		-- Ignore this event if `WinEnter` just happened.
		if just_entered_window == true then
			just_entered_window = false
			return
		end

		if internal.unreserve_window(vim.api.nvim_get_current_win()) then
			vim.api.nvim_exec_autocmds("VimResized", {})
		end
	end,
})

return internal


================================================
FILE: lua/detour/movements.lua
================================================
---@mod detour.movements
---@brief [[
---Detour.nvim breaks window navigation commands such as
---`<C-w>w`, `<C-w>j`, or `vim.cmd.wincmd("h")`. Instead of
---using those, the user MUST use the following window navigation
---commands that this plugin provides that implements moving
---between windows while skipping over windows covered by detours.
---
---NOTE: Regular window movements such as `<C-w>w`, `<C-w>j`,
---`vim.cmd.wincmd("h")` should still work in automated
---scripts/functions. Still, you may find it more useful to use detour's
---"detour-aware" movement functions in your scripts/functions as well.
---@brief ]]

---@tag detour-movements

local movements = {}

local util = require("detour.util")
local internal = require("detour.internal")

local module_augroup = "detour-movements"
vim.api.nvim_create_augroup(module_augroup, { clear = true })

---DO NOT USE. FOR TESTING ONLY.
movements._safe_state_handler = function()
	internal.garbage_collect()
	vim.fn.win_gotoid(util.find_top_popup())
end

-- Sometimes the cursor can end up behind a detour (eg, when a window is
-- closed). In these cases just move the cursor to an appropriate place.
-- Using SafeState here means this autocmd will not interfere with automated
-- movements.
vim.api.nvim_create_autocmd({ "SafeState" }, {
	group = module_augroup,
	callback = movements._safe_state_handler,
	nested = true,
})

--- Switch to a window to the right. Skip over any non-floating windows
--- covered by a detour.
---@return nil
---@usage `
--- local detour_moves = require("detour.movements")
--- vim.keymap.set({ "n", "t" }, "<C-l>", detour_moves.DetourWinCmdL)
--- vim.keymap.set({ "n", "t" }, "<C-w>l", detour_moves.DetourWinCmdL)
--- vim.keymap.set({ "n", "t" }, "<C-w><C-l>", detour_moves.DetourWinCmdL)
---`
function movements.DetourWinCmdL()
	local covered_bases = util.find_covered_bases(
		vim.api.nvim_get_current_win()
	) or { vim.api.nvim_get_current_win() }
	table.sort(covered_bases, function(windowA, windowB)
		local _, _, _, rightA = util.get_text_area_dimensions(windowA)
		local _, _, _, rightB = util.get_text_area_dimensions(windowB)
		return rightA > rightB
	end)
	local base = covered_bases[1]
	for _, window in ipairs(covered_bases) do
		local _, _, _, right = util.get_text_area_dimensions(window)
		if right ~= vim.o.columns then
			base = window
			break
		end
	end

	vim.fn.win_gotoid(base)
	vim.cmd.wincmd("l")
	-- It's possible to rely on the SafeState autocmd instead of explicitly
	-- moving to the top popup, but an explicit call allows this function to work
	-- properly when used in other code.
	vim.fn.win_gotoid(util.find_top_popup())
end

--- Switch to a window to the left. Skip over any non-floating windows
--- covered by a detour.
---@return nil
---@usage `
--- local detour_moves = require("detour.movements")
--- vim.keymap.set({ "n", "t" }, "<C-h>", detour_moves.DetourWinCmdH)
--- vim.keymap.set({ "n", "t" }, "<C-w>h", detour_moves.DetourWinCmdH)
--- vim.keymap.set({ "n", "t" }, "<C-w><C-h>", detour_moves.DetourWinCmdH)
---`
function movements.DetourWinCmdH()
	local covered_bases = util.find_covered_bases(
		vim.api.nvim_get_current_win()
	) or { vim.api.nvim_get_current_win() }
	table.sort(covered_bases, function(windowA, windowB)
		local _, _, leftA, _ = util.get_text_area_dimensions(windowA)
		local _, _, leftB, _ = util.get_text_area_dimensions(windowB)
		return leftA < leftB
	end)
	local base = covered_bases[1]
	for _, window in ipairs(covered_bases) do
		local _, _, left, _ = util.get_text_area_dimensions(window)
		if left ~= 0 then
			base = window
			break
		end
	end

	vim.fn.win_gotoid(base)
	vim.cmd.wincmd("h")
	vim.fn.win_gotoid(util.find_top_popup())
end

--- Switch to a window below. Skip over any non-floating windows
--- covered by a detour.
---@return nil
---@usage `
--- local detour_moves = require("detour.movements")
--- vim.keymap.set({ "n", "t" }, "<C-j>", detour_moves.DetourWinCmdJ)
--- vim.keymap.set({ "n", "t" }, "<C-w>j", detour_moves.DetourWinCmdJ)
--- vim.keymap.set({ "n", "t" }, "<C-w><C-j>", detour_moves.DetourWinCmdJ)
---`
function movements.DetourWinCmdJ()
	local covered_bases = util.find_covered_bases(
		vim.api.nvim_get_current_win()
	) or { vim.api.nvim_get_current_win() }
	table.sort(covered_bases, function(windowA, windowB)
		local _, bottomA, _, _ = util.get_text_area_dimensions(windowA)
		local _, bottomB, _, _ = util.get_text_area_dimensions(windowB)
		return bottomA > bottomB
	end)
	local base = covered_bases[1]
	for _, window in ipairs(covered_bases) do
		local _, bottom, _, _ = util.get_text_area_dimensions(window)
		if
			bottom
			~= vim.o.lines
				- vim.o.cmdheight
				- (vim.o.laststatus == 0 and 0 or 1)
		then -- subtract one for statusline
			base = window
			break
		end
	end

	vim.fn.win_gotoid(base)
	vim.cmd.wincmd("j")
	vim.fn.win_gotoid(util.find_top_popup())
end

--- Switch to a window above. Skip over any non-floating windows
--- covered by a detour.
---@return nil
---@usage `
--- local detour_moves = require("detour.movements")
--- vim.keymap.set({ "n", "t" }, "<C-k>", detour_moves.DetourWinCmdK)
--- vim.keymap.set({ "n", "t" }, "<C-w>k", detour_moves.DetourWinCmdK)
--- vim.keymap.set({ "n", "t" }, "<C-w><C-k>", detour_moves.DetourWinCmdK)
---`
function movements.DetourWinCmdK()
	local covered_bases = util.find_covered_bases(
		vim.api.nvim_get_current_win()
	) or { vim.api.nvim_get_current_win() }
	table.sort(covered_bases, function(windowA, windowB)
		local topA, _, _, _ = util.get_text_area_dimensions(windowA)
		local topB, _, _, _ = util.get_text_area_dimensions(windowB)
		return topA < topB
	end)
	local base = covered_bases[1]
	for _, window in ipairs(covered_bases) do
		local top, _, _, _ = util.get_text_area_dimensions(window)
		if top ~= 0 then
			base = window
			break
		end
	end

	vim.fn.win_gotoid(base)
	vim.cmd.wincmd("k")
	vim.fn.win_gotoid(util.find_top_popup())
end

--- Switch windows in a cycle. Skip over any non-floating windows covered
--- by a detour.
---@return nil
---@usage `
--- local detour_moves = require("detour.movements")
--- vim.keymap.set({ "n", "t" }, "<C-w>", detour_moves.DetourWinCmdW)
--- vim.keymap.set({ "n", "t" }, "<C-w>w", detour_moves.DetourWinCmdW)
--- vim.keymap.set({ "n", "t" }, "<C-w><C-w>", detour_moves.DetourWinCmdW)
---`
function movements.DetourWinCmdW()
	-- We do not just repeatedly do `vim.cmd.wincmd("w")` until we hit a detour
	-- or uncovered window because doing so could form a cycle that does not
	-- include all windows.
	local windows = vim.api.nvim_tabpage_list_wins(0)
	-- TODO: if there are no (visible) detours, just call vim.cmd.wincmd("w").
	local current_top = util.find_top_popup()

	-- Collect all the top detour or naked base windows.
	local tops = {}
	for _, window in ipairs(windows) do
		tops[util.find_top_popup(window)] = true
	end

	-- Sort all the top windows. Filter out the windows ordered before the
	-- current top.
	local ordered_tops = {}
	for top in vim.spairs(tops) do
		if top > current_top then
			ordered_tops[#ordered_tops + 1] = top
		end
	end

	-- In tops is empty, add the first top so that we cycle back to the
	-- beginning of the list.
	for top in vim.spairs(tops) do
		ordered_tops[#ordered_tops + 1] = top
		break
	end

	vim.fn.win_gotoid(ordered_tops[1])
end

return movements


================================================
FILE: lua/detour/show_path_in_title.lua
================================================
---@mod detour.show_path_in_title
---Internal helper for updating popup titles.

-- Put this code in its own library to make sure it's only run once.

local util = require("detour.util")
local internal = require("detour.internal")

---@type integer
local ns = vim.api.nvim_create_namespace("detour.nvim-ns")

---@type uv.uv_timer_t
local timer = assert(vim.uv.new_timer())

-- This implements a trailing debouce where each call to the debounced function
-- will start a timer and cancel any existing timers for that function. The
-- function will eventually be called with the arguments from its most recent
-- call.
---@param ms integer
---@param fn fun(...: any)
---@return fun(...: any)
local function debounce(ms, fn)
	return function(...)
		local argv = { ... }
		timer:stop()
		timer:start(ms, 0, function()
			vim.schedule_wrap(fn)(unpack(argv))
		end)
	end
end

---@param _ any
---@param window_id integer
local function update_title(_, window_id)
    if vim.tbl_contains(internal.list_popups(), window_id) then
        vim.api.nvim_exec_autocmds("User", {
            pattern = "DetourUpdateTitle" .. util.stringify(window_id),
        })
    end
end

-- The reason why we're using a callback on decoration_provider instead of using an autocmd on BufEnter is because we
-- want to trigger a title update while browsing through netrw directories and that doesn't trigger BufEnter.
--
-- From `api.txt`:
-- (About nvim_set_decoration_provider) doing anything other than setting
-- extmarks is considered experimental. Doing things like changing options are
-- not explicitly forbidden, but is likely to have unexpected consequences (such
-- as 100% CPU consumption). Doing `vim.rpcnotify` should be OK, but
-- `vim.rpcrequest` is quite dubious for the moment.
--
-- I debounce `update_title` since rapidly changing the title with `on_win`
-- causes neovim to freeze.
vim.api.nvim_set_decoration_provider(ns, {
	on_win = debounce(50, update_title),
})
return {}


================================================
FILE: lua/detour/util.lua
================================================
---@mod detour.util
---Utilities for window geometry, state checks, and helpers.

local util = {}

---@class detour.util
---@field Set fun(list: any[]): table<any, boolean>
---@field contains_element fun(array: any[], target: any): boolean
---@field contains_key fun(array: table, target: any): boolean
---@field contains_value fun(array: table, target: any): boolean
---@field get_text_area_dimensions fun(window_id: integer): integer, integer, integer, integer
---@field is_floating fun(window_id: integer): boolean
---@field get_maybe_zindex fun(window_id: integer): integer|nil
---@field overlap fun(window_a: integer, window_b: integer): boolean
---@field find_top_popup fun(window?: integer): integer
---@field find_covered_bases fun(window_id: integer): integer[]|nil
---@field find_covered_windows fun(window_id: integer): integer[]
---@field is_open fun(window_id: integer): boolean
---@field stringify fun(number: integer): string
---@field pairs_by_keys fun(t: table, f?: fun(a:any,b:any):boolean): fun(): any, any
---@field is_statusline_global fun(): boolean
---@field base_at_screenpos fun(tab_id: integer, screenrow: integer, screencol: integer): integer|nil

local internal = require("detour.internal")

---@param list any[]
---@return table<any, boolean>
function util.Set(list)
	local set = {}
	for _, l in ipairs(list) do
		set[l] = true
	end
	return set
end

---@param array any[]
---@param target any
---@return boolean
function util.contains_element(array, target)
	for _, value in ipairs(array) do
		if value == target then
			return true
		end
	end
	return false
end

---@param array table
---@param target any
---@return boolean
function util.contains_key(array, target)
	for key, _ in pairs(array) do
		if key == target then
			return true
		end
	end
	return false
end

---@param array table
---@param target any
---@return boolean
function util.contains_value(array, target)
	for _, value in pairs(array) do
		if value == target then
			return true
		end
	end
	return false
end

--- Returns the positions of top, bottom, left, and right of a given window's text area.
--- The statusline is not included in the text area. Bottom and right are exclusive.
---@param window_id integer
---@return integer top, integer bottom, integer left, integer right
function util.get_text_area_dimensions(window_id)
	local top, left = unpack(vim.api.nvim_win_get_position(window_id))
	local bottom = top + vim.api.nvim_win_get_height(window_id)
	local right = left + vim.api.nvim_win_get_width(window_id)
	return top, bottom, left, right
end

---@param window_id integer
---@return boolean
function util.is_floating(window_id)
	return vim.api.nvim_win_get_config(window_id).relative ~= ""
end

--- Returns the zindex for the given window, if floating, otherwise nil.
---@param window_id integer
---@return integer|nil
function util.get_maybe_zindex(window_id)
	return vim.api.nvim_win_get_config(window_id).zindex
end

---@param positions_a { [1]: integer, [2]: integer, [3]: integer, [4]: integer }
---@param positions_b { [1]: integer, [2]: integer, [3]: integer, [4]: integer }
---@return boolean
local function overlap_helper(positions_a, positions_b)
	local top_a, bottom_a, left_a, right_a = unpack(positions_a)
	local top_b, bottom_b, left_b, right_b = unpack(positions_b)
	if math.max(left_a, left_b) >= math.min(right_a, right_b) then
		return false
	end

	if math.max(top_a, top_b) >= math.min(bottom_a, bottom_b) then
		return false
	end

	return true
end

---@param window_a integer
---@param window_b integer
---@return boolean
function util.overlap(window_a, window_b)
	return overlap_helper(
		{ util.get_text_area_dimensions(window_a) },
		{ util.get_text_area_dimensions(window_b) }
	)
end

---@param window? integer
---@return integer window_id
function util.find_top_popup(window)
	local window_id = window or vim.api.nvim_get_current_win()
	local all_coverable_windows = internal.list_reserved_windows()
	for _, popup in ipairs(internal.list_popups()) do
		if
			not vim.list_contains(all_coverable_windows, popup) -- ignore popups with popups nested in them
			and vim.tbl_contains(util.find_covered_windows(popup), window_id)
		then
			return popup
		end
	end
	return window_id -- no popup that covers the current window was found
end

-- Finds all base windows that are covered by the provided popup.
-- If the provided window is not a popup, returns the given argument.
---@param window_id integer
---@return integer[]|nil
function util.find_covered_bases(window_id)
	assert(util.is_open(window_id), tostring(window_id) .. " is not open")
	local current_window = window_id
	local coverable_bases = nil
	while internal.get_reserved_windows(current_window) do
		coverable_bases = internal.get_reserved_windows(current_window) or {}
		assert(
			#coverable_bases > 0,
			"[detour.nvim] There should never be an empty array in popup_to_covered_windows."
		)
		-- We iterate on only the first covered window because there are two cases:
		-- A: there is exactly one covered window and it's another detour.
		-- 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.
		current_window = coverable_bases[1]
	end

	-- This covers the case where the window_id is not a detour popup and we never enter the above loop.
	if coverable_bases == nil then
		return nil
	end

	return vim.tbl_filter(function(base)
		return util.overlap(base, window_id)
	end, coverable_bases)
end

-- Finds all windows that are covered by the provided popup.
-- If the provided window is not a popup, returns the given argument.
---@param window integer
---@return integer[]
function util.find_covered_windows(window)
	local current_window = window
	local coverable_windows = {}
	while internal.get_reserved_windows(current_window) do
		coverable_windows[#coverable_windows + 1] =
			internal.get_reserved_windows(current_window)
		assert(
			#coverable_windows[#coverable_windows] > 0,
			"[detour.nvim] There should never be an empty array in popup_to_covered_windows."
		)
		-- We iterate on only the first covered window because there are two cases:
		-- A: there is exactly one covered window and it's another detour.
		-- 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.
		current_window = coverable_windows[#coverable_windows][1]
	end

	if #coverable_windows == 0 then
		return { window }
	end

	return vim.iter(coverable_windows)
		:flatten()
		:filter(function(other)
			return util.overlap(other, window)
		end)
		:totable()
end

---@param window_id integer
---@return boolean
function util.is_open(window_id)
	return vim.tbl_contains(vim.api.nvim_list_wins(), window_id)
end

---@param number integer
---@return string
function util.stringify(number)
	local base = string.byte("a")
	local values = {}
	for digit in ("" .. number):gmatch(".") do
		values[#values + 1] = tonumber(digit) + base
	end
	return string.char(unpack(values))
end

---@param t table
---@param f? fun(a:any,b:any):boolean
---@return fun(): any, any
function util.pairs_by_keys(t, f)
	local a = {}
	for n in pairs(t) do
		table.insert(a, n)
	end
	table.sort(a, f)
	local i = 0 -- iterator variable
	local iter = function() -- iterator function
		i = i + 1
		if a[i] == nil then
			return nil
		else
			return a[i], t[a[i]]
		end
	end
	return iter
end

---Whether the statusline is global (laststatus == 3).
---@return boolean
function util.is_statusline_global()
	-- When laststatus == 3, Neovim uses a single global statusline
	-- and individual windows do not have their own.
	return vim.o.laststatus == 3
end

---Find non-floating window at a given screen position (1-based)
---@param tab_id integer
---@param screenrow integer
---@param screencol integer
---@return integer|nil
function util.base_at_screenpos(tab_id, screenrow, screencol)
	for _, win in ipairs(vim.api.nvim_tabpage_list_wins(tab_id)) do
		if not util.is_floating(win) then
			local winnr = vim.fn.win_id2win(win)
			assert(winnr ~= 0, tostring(win) .. " was not found")
			local top, left = unpack(vim.fn.win_screenpos(winnr))
			assert(top ~= 0 and left ~= 0, tostring(winnr) .. " was not found")
			local width = vim.fn.winwidth(winnr) -- includes signs/number column
			local height = vim.fn.winheight(winnr)
			if
				screenrow >= top
				and screenrow
					< top
						+ height
						+ (util.is_statusline_global() and 0 or 1) -- for per-window statusline
						+ 1 -- for the border and global statusline
				and screencol >= left
				and screencol < left + width
			then
				return win
			end
		end
	end
	return nil
end

return util


================================================
FILE: lua/detour/windowing_algorithm.lua
================================================
---@mod detour.windowing_algorithm
---Layout algorithm for computing popup positions.

local algo = {}

local util = require("detour.util")

---Compute float options to cover the given windows without overlapping others.
---@param coverable_windows integer[]
---@param tab_id integer
---@return table|nil
function algo.construct_window_opts(coverable_windows, tab_id)
	local roots = {}
	for _, window_id in ipairs(vim.api.nvim_tabpage_list_wins(tab_id)) do
		if not util.is_floating(window_id) then
			table.insert(roots, window_id)
		end
	end

	local uncoverable_windows = {}
	for _, root in ipairs(roots) do
		if not util.contains_element(coverable_windows, root) then
			table.insert(uncoverable_windows, root)
		end
	end
	local horizontals = {}
	local verticals = {}

	for _, root in ipairs(roots) do
		local top, bottom, left, right = util.get_text_area_dimensions(root)
		horizontals[top] = 1
		horizontals[bottom] = 1
		verticals[left] = 1
		verticals[right] = 1
	end

	local floors = {}
	local sides = {}

	for top, _ in pairs(horizontals) do
		for bottom, _ in pairs(horizontals) do
			if top < bottom then
				table.insert(floors, { top, bottom })
			end
		end
	end

	for left, _ in pairs(verticals) do
		for right, _ in pairs(verticals) do
			if left < right then
				table.insert(sides, { left, right })
			end
		end
	end

	local max_area = 0
	local dimensions = nil
	for _, curr_floors in ipairs(floors) do
		local top, bottom = unpack(curr_floors)
		for _, curr_sides in ipairs(sides) do
			local left, right = unpack(curr_sides)
			local legal = true
			for _, uncoverable_window in ipairs(uncoverable_windows) do
				local uncoverable_top, uncoverable_bottom, uncoverable_left, uncoverable_right =
					util.get_text_area_dimensions(uncoverable_window)
				if not util.is_statusline_global() then -- we have to worry about statuslines
					-- The fact that we're starting with text area dimensions
					-- means that all of the rectangles we are working with do
					-- not include statuslines. This means that we need to avoid
					-- inadvertantly covering a window's status line. Neovim can
					-- be configured to only show statuslines when there are
					-- multiple windows on the screen but we can ignore that
					-- because this loop only runs when there are multiple
					-- windows on the screen.
					uncoverable_top = uncoverable_top - 1 -- don't cover above window's statusline
					uncoverable_bottom = uncoverable_bottom + 1 -- don't cover this window's statusline
				end
				local lowest_top = math.max(top, uncoverable_top)
				local highest_bottom = math.min(bottom, uncoverable_bottom)
				local rightest_left = math.max(left, uncoverable_left)
				local leftest_right = math.min(right, uncoverable_right)
				if
					(lowest_top < highest_bottom)
					and (rightest_left < leftest_right)
				then
					legal = false
				end
			end

			local area = (bottom - top) * (right - left)
			if legal and (area > max_area) then
				dimensions = { top, bottom, left, right }
				max_area = area
			end
		end
	end

	if dimensions == nil then
		vim.api.nvim_echo(
			{ { "[detour.nvim] was unable to find a spot to create a popup." } },
			true,
			{ err = true }
		)
		return nil
	end

	local top, bottom, left, right = unpack(dimensions)
	local width = right - left
	local height = bottom - top

	if height < 1 then
		vim.api.nvim_echo({
			{
				"[detour.nvim] (please file a github issue!) height is supposed to be at least 1.",
			},
		}, true, { err = true })
		return nil
	end
	if width < 1 then
		vim.api.nvim_echo({
			{
				"[detour.nvim] (please file a github issue!) width is supposed to be at least 1.",
			},
		}, true, { err = true })
		return nil
	end

	-- If a window's height extends below the UI, the window's border gets cut off.
	-- If a window's width extends beyond the UI, the window's border still shows up at the end of the UI.
	-- Using a border adds 2 to the window's height and width.
	local window_opts = {
		relative = "editor",
		row = top,
		col = left,
		width = (width - 2 > 0) and (width - 2) or width, -- create some space for borders
		height = (height - 2 > 0) and (height - 2) or height, -- create some space for borders
		border = "rounded",
		zindex = 1,
	}

	if window_opts.width > 4 then
		window_opts.width = window_opts.width - 4
		window_opts.col = window_opts.col + 2
	end

	if window_opts.height > 2 then
		window_opts.height = window_opts.height - 2
		window_opts.row = window_opts.row + 1
	end

	return window_opts
end

---Construct nested float window opts within a parent float.
---@param parent integer
---@param layer integer?
---@return table
function algo.construct_nest(parent, layer)
	assert(
		util.is_open(parent),
		"trying to construct a nested window in a window that doesn't exist: ",
		parent
	)
	local top, bottom, left, right = util.get_text_area_dimensions(parent)
	local width = right - left
	local height = bottom - top
	local border = "rounded"
	if height >= 3 then
		height = height - 2
		top = top + 1
	end
	if width >= 3 then
		width = width - 2
		left = left + 1
	end
	return {
		relative = "editor",
		row = top,
		col = left,
		width = width,
		height = height,
		border = border,
		zindex = layer,
	}
end

return algo


================================================
FILE: plugin/detour.lua
================================================
local detour = require("detour")
local features = require("detour.features")

vim.api.nvim_create_user_command("Detour", detour.Detour, {})

vim.api.nvim_create_user_command(
	"DetourCurrentWindow",
	detour.DetourCurrentWindow,
	{}
)

vim.api.nvim_create_user_command(
	"DetourUncoverWindowWithMouse",
	features.UncoverWindowWithMouse,
	{}
)

vim.api.nvim_create_user_command(
	"DetourHideAllDetours",
	features.HideAllDetours,
	{}
)

vim.api.nvim_create_user_command(
	"DetourRevealAllDetours",
	features.RevealAllDetours,
	{}
)

vim.api.nvim_create_user_command(
	"DetourCloseCurrentStack",
	features.CloseCurrentStack,
	{}
)


================================================
FILE: run-in-docker.sh
================================================
#!/bin/sh
nvim -u NONE \
  -c "lua local k,l,_=pcall(require,'luarocks.loader') _=k and l.add_context('busted','$BUSTED_VERSION')" \
  -l "/usr/local/lib/luarocks/rocks-5.1/busted/$BUSTED_VERSION/bin/busted" .


================================================
FILE: spec/config_spec.lua
================================================
local detour = require("detour")
local config = require("detour.config")

describe("detour config", function()
	before_each(function()
		vim.g.detour_testing = true
		vim.cmd([[ 
      %bwipeout!
      mapclear
      nmapclear
      vmapclear
      xmapclear
      smapclear
      omapclear
      mapclear
      imapclear
      lmapclear
      cmapclear
      tmapclear
    ]])
		vim.api.nvim_clear_autocmds({}) -- delete any autocmds not in a group
		for _, autocmd in ipairs(vim.api.nvim_get_autocmds({})) do
			if vim.startswith(autocmd.group_name, "detour-") then
				vim.api.nvim_del_autocmd(autocmd.id)
			end
		end
		vim.o.splitbelow = true
		vim.o.splitright = true
		-- Reset to default configuration for each test
		config.setup({ title = "path" })
	end)

	it("disables titles when title = 'none'", function()
		config.setup({ title = "none" })
		assert(detour.Detour())
		-- With title disabled, floating window should not have a title
		assert.is_nil(vim.api.nvim_win_get_config(0).title)
	end)

	it("rejects invalid options and keeps previous config", function()
		local before = config.options.title
		config.setup({ title = "not-a-valid-option" })
		assert.same(before, config.options.title)
	end)
end)


================================================
FILE: spec/detour_auto_unreserve_spec.lua
================================================
local detour = require("detour")
local util = require("detour.util")

describe("detour auto-unreserve on interaction", function()
	before_each(function()
		vim.g.detour_testing = true
		vim.cmd([[ 
      %bwipeout!
      mapclear
      nmapclear
      vmapclear
      xmapclear
      smapclear
      omapclear
      mapclear
      imapclear
      lmapclear
      cmapclear
      tmapclear
    ]])
		vim.api.nvim_clear_autocmds({}) -- delete any autocmds not in a group
		for _, autocmd in ipairs(vim.api.nvim_get_autocmds({ pattern = "*" })) do
			if vim.startswith(autocmd.group_name, "detour-") then
				vim.api.nvim_del_autocmd(autocmd.id)
			end
		end
		vim.o.splitbelow = true
		vim.o.splitright = true
	end)

	it("unreserves interacted window and resizes popup", function()
		-- Create a 2-column layout
		local left_base = vim.api.nvim_get_current_win()
		vim.cmd.vsplit()
		local right_base = vim.api.nvim_get_current_win()

		-- Create a detour covering both base windows
		local popup = assert(detour.Detour())
		assert.True(util.overlap(popup, right_base))

		-- Focus the left base window, then simulate user interaction
		vim.cmd.split()
		vim.api.nvim_exec_autocmds("VimResized", {}) -- trigger detour resize
		assert.False(util.overlap(popup, right_base))
		vim.fn.win_gotoid(right_base)
		vim.cmd.startinsert() -- right_base should now be unreserved

		assert.are.same(util.find_covered_windows(popup), { left_base })
	end)
end)


================================================
FILE: spec/detour_close_stack_spec.lua
================================================
local detour = require("detour")
local util = require("detour.util")
local features = require("detour.features")

describe("features.CloseCurrentStack", function()
	before_each(function()
		vim.g.detour_testing = true
		vim.cmd([[
      %bwipeout!
      mapclear
      nmapclear
      vmapclear
      xmapclear
      smapclear
      omapclear
      mapclear
      imapclear
      lmapclear
      cmapclear
      tmapclear
    ]])
		vim.api.nvim_clear_autocmds({}) -- delete any autocmds not in a group
		for _, autocmd in ipairs(vim.api.nvim_get_autocmds({ pattern = "*" })) do
			if vim.startswith(autocmd.group_name, "detour-") then
				vim.api.nvim_del_autocmd(autocmd.id)
			end
		end
		vim.o.splitbelow = true
		vim.o.splitright = true
	end)

	it("closes a single detour and returns to base", function()
		local base = vim.api.nvim_get_current_win()
		local popup = assert(detour.Detour())

		features.CloseCurrentStack()

		assert.False(util.is_open(popup))
		assert.same({ base }, vim.api.nvim_list_wins())
	end)

	it("closes all nested detours in the chain", function()
		local original = vim.api.nvim_get_current_win()
		vim.cmd.split()
		local base = vim.api.nvim_get_current_win()
		local parent = assert(detour.Detour())
		local child = assert(detour.Detour())
		local grandchild = assert(detour.Detour())

		features.CloseCurrentStack()

		assert.False(util.is_open(parent))
		assert.False(util.is_open(child))
		assert.False(util.is_open(grandchild))
		assert.same({ original, base }, vim.api.nvim_list_wins())
		vim.api.nvim_win_close(base, true)
	end)

	it("is a no-op when not inside a detour", function()
		vim.cmd.split()
		local base = vim.api.nvim_get_current_win()
		local before = vim.api.nvim_list_wins()

		features.CloseCurrentStack()

		local after = vim.api.nvim_list_wins()
		assert.same(before, after)
		assert.same(base, vim.api.nvim_get_current_win())
		vim.cmd.close()
	end)
end)


================================================
FILE: spec/detour_hide_reveal_spec.lua
================================================
local detour = require("detour")
local util = require("detour.util")
local features = require("detour.features")

describe("detour hide/reveal", function()
	before_each(function()
		vim.g.detour_testing = true
		vim.cmd([[
        %bwipeout!
        mapclear
        nmapclear
        vmapclear
        xmapclear
        smapclear
        omapclear
        mapclear
        imapclear
        lmapclear
        cmapclear
        tmapclear
        ]])
		vim.api.nvim_clear_autocmds({})
		for _, autocmd in ipairs(vim.api.nvim_get_autocmds({ pattern = "*" })) do
			if vim.startswith(autocmd.group_name, "detour-") then
				vim.api.nvim_del_autocmd(autocmd.id)
			end
		end
		vim.o.splitbelow = true
		vim.o.splitright = true
	end)

	it("HideAllDetours and RevealAllDetours toggle popup visibility", function()
		-- Create two side-by-side base windows
		vim.cmd.vsplit()
		vim.cmd.wincmd("h")
		local left_base = vim.api.nvim_get_current_win()
		vim.cmd.wincmd("l")
		local right_base = vim.api.nvim_get_current_win()

		-- Create a detour over each base window
		vim.fn.win_gotoid(right_base)
		local right_popup = assert(detour.DetourCurrentWindow())
		vim.fn.win_gotoid(left_base)
		local left_popup = assert(detour.DetourCurrentWindow())

		-- Initially, popups are visible
		assert.False(vim.api.nvim_win_get_config(left_popup).hide or false)
		assert.False(vim.api.nvim_win_get_config(right_popup).hide or false)

		-- Hide all detours
		features.HideAllDetours()
		assert.True(vim.api.nvim_win_get_config(left_popup).hide)
		assert.True(vim.api.nvim_win_get_config(right_popup).hide)

		-- Reveal all detours
		features.RevealAllDetours()
		assert.False(vim.api.nvim_win_get_config(left_popup).hide)
		assert.False(vim.api.nvim_win_get_config(right_popup).hide)
	end)
end)


================================================
FILE: spec/detour_movements_spec.lua
================================================
local detour = require("detour")
local movements = require("detour.movements")
local util = require("detour.util")

function Set(list)
	local set = {}
	for _, l in ipairs(list) do
		set[l] = true
	end
	return set
end

describe("detour", function()
	before_each(function()
		vim.cmd([[
        %bwipeout!
        mapclear
        nmapclear
        vmapclear
        xmapclear
        smapclear
        omapclear
        mapclear
        imapclear
        lmapclear
        cmapclear
        tmapclear
        ]])
		vim.api.nvim_clear_autocmds({}) -- delete any autocmds not in a group
		for _, autocmd in ipairs(vim.api.nvim_get_autocmds({ pattern = "*" })) do
			if vim.startswith(autocmd.group_name, "detour-") then
				vim.api.nvim_del_autocmd(autocmd.id)
			end
		end
		vim.o.splitbelow = true
		vim.o.splitright = true
	end)

	it("Switching horizontally between detours", function()
		local left_base = vim.api.nvim_get_current_win()

		vim.cmd.vsplit()
		local middle_base = vim.api.nvim_get_current_win()

		vim.cmd.vsplit()
		local right_base = vim.api.nvim_get_current_win()

		vim.fn.win_gotoid(middle_base)
		local middle_popup = assert(detour.DetourCurrentWindow())

		-- Enter and leave a popup from a base window in both directions
		movements.DetourWinCmdH()
		assert.same(vim.api.nvim_get_current_win(), left_base)

		movements.DetourWinCmdL()
		assert.same(vim.api.nvim_get_current_win(), middle_popup)

		movements.DetourWinCmdL()
		assert.same(vim.api.nvim_get_current_win(), right_base)

		movements.DetourWinCmdH()
		assert.same(vim.api.nvim_get_current_win(), middle_popup)

		-- Create nested popup
		local middle_nested_popup = assert(detour.DetourCurrentWindow())

		-- Enter and leave a nested popup from a base window in both directions
		movements.DetourWinCmdH()
		assert.same(vim.api.nvim_get_current_win(), left_base)

		movements.DetourWinCmdL()
		assert.same(vim.api.nvim_get_current_win(), middle_nested_popup)

		movements.DetourWinCmdL()
		assert.same(vim.api.nvim_get_current_win(), right_base)

		movements.DetourWinCmdH()
		assert.same(vim.api.nvim_get_current_win(), middle_nested_popup)

		-- Create popups on left and right
		vim.fn.win_gotoid(left_base)
		local left_popup = assert(detour.DetourCurrentWindow())

		vim.fn.win_gotoid(right_base)
		local right_popup = assert(detour.DetourCurrentWindow())

		vim.fn.win_gotoid(middle_nested_popup)

		-- Enter and leave a nested popup from a non-nested popup in both directions
		movements.DetourWinCmdH()
		assert.same(vim.api.nvim_get_current_win(), left_popup)

		movements.DetourWinCmdL()
		assert.same(vim.api.nvim_get_current_win(), middle_nested_popup)

		movements.DetourWinCmdL()
		assert.same(vim.api.nvim_get_current_win(), right_popup)

		movements.DetourWinCmdH()
		assert.same(vim.api.nvim_get_current_win(), middle_nested_popup)
	end)

	it("Switching vertically between detours", function()
		local top_base = vim.api.nvim_get_current_win()

		vim.cmd.split()
		local middle_base = vim.api.nvim_get_current_win()

		vim.cmd.split()
		local bottom_base = vim.api.nvim_get_current_win()

		vim.fn.win_gotoid(middle_base)
		local middle_popup = assert(detour.DetourCurrentWindow())

		-- Enter and leave a popup from a base window in both directions
		movements.DetourWinCmdK()
		assert.same(vim.api.nvim_get_current_win(), top_base)

		movements.DetourWinCmdJ()
		assert.same(vim.api.nvim_get_current_win(), middle_popup)

		movements.DetourWinCmdJ()
		assert.same(vim.api.nvim_get_current_win(), bottom_base)

		movements.DetourWinCmdK()
		assert.same(vim.api.nvim_get_current_win(), middle_popup)

		-- Create nested popup
		local middle_nested_popup = assert(detour.DetourCurrentWindow())

		-- Enter and leave a nested popup from a base window in both directions
		movements.DetourWinCmdK()
		assert.same(vim.api.nvim_get_current_win(), top_base)

		movements.DetourWinCmdJ()
		assert.same(vim.api.nvim_get_current_win(), middle_nested_popup)

		movements.DetourWinCmdJ()
		assert.same(vim.api.nvim_get_current_win(), bottom_base)

		movements.DetourWinCmdK()
		assert.same(vim.api.nvim_get_current_win(), middle_nested_popup)

		-- Create popups on top and bottom
		vim.fn.win_gotoid(top_base)
		local top_popup = assert(detour.DetourCurrentWindow())

		vim.fn.win_gotoid(bottom_base)
		local bottom_popup = assert(detour.DetourCurrentWindow())

		vim.fn.win_gotoid(middle_nested_popup)

		-- Enter and leave a nested popup from a non-nested popup in both directions
		movements.DetourWinCmdK()
		assert.same(vim.api.nvim_get_current_win(), top_popup)

		movements.DetourWinCmdJ()
		assert.same(vim.api.nvim_get_current_win(), middle_nested_popup)

		movements.DetourWinCmdJ()
		assert.same(vim.api.nvim_get_current_win(), bottom_popup)

		movements.DetourWinCmdK()
		assert.same(vim.api.nvim_get_current_win(), middle_nested_popup)
	end)

	it("Switch windows with <C-w>w", function()
		local base = vim.api.nvim_get_current_win()
		local popup = detour.Detour()

		vim.cmd.split()
		local bottom_base = vim.api.nvim_get_current_win()
		assert.is_not.same(bottom_base, popup)

		movements.DetourWinCmdW()

		assert.same(popup, vim.api.nvim_get_current_win())

		movements.DetourWinCmdW()
		assert.same(bottom_base, vim.api.nvim_get_current_win())
	end)

	it("Move cursor on SafeState", function()
		local base = vim.api.nvim_get_current_win()
		local popup = detour.Detour()

		vim.opt.eventignore = "all" -- deactivate plugin
		vim.api.nvim_set_current_win(base)
		vim.opt.eventignore = "" -- reactivate plugin
		movements._safe_state_handler()
		assert.same(popup, vim.api.nvim_get_current_win())
	end)
end)


================================================
FILE: spec/detour_spec.lua
================================================
local detour = require("detour")
local util = require("detour.util")
local features = require("detour.features")

function Set(list)
	local set = {}
	for _, l in ipairs(list) do
		set[l] = true
	end
	return set
end

describe("detour", function()
	before_each(function()
		vim.g.detour_testing = true
		vim.cmd([[
        %bwipeout!
        mapclear
        nmapclear
        vmapclear
        xmapclear
        smapclear
        omapclear
        mapclear
        imapclear
        lmapclear
        cmapclear
        tmapclear
        ]])
		vim.api.nvim_clear_autocmds({}) -- delete any autocmds not in a group
		for _, autocmd in ipairs(vim.api.nvim_get_autocmds({})) do
			if vim.startswith(autocmd.group_name, "detour-") then
				vim.api.nvim_del_autocmd(autocmd.id)
			end
		end
		vim.o.splitbelow = true
		vim.o.splitright = true
	end)

	-- See Issue #25 for a discussion of duplication in this test suite.
	it("create popup", function()
		vim.cmd.view("/tmp/detour")
		local before_buffer = vim.api.nvim_get_current_buf()
		local before_window = vim.api.nvim_get_current_win()

		local after_window = assert(detour.Detour())
		local after_buffer = vim.api.nvim_get_current_buf()
		assert.are_not.same(before_window, after_window)
		assert.are.same(before_buffer, after_buffer)
		assert.True(util.is_floating(after_window))
		assert.same(#vim.api.nvim_list_wins(), 2)
	end)

	it("create nested popup over non-Detour popup", function()
		local base_buffer = vim.api.nvim_get_current_buf()
		vim.api.nvim_open_win(
			vim.api.nvim_win_get_buf(0),
			true,
			{ relative = "win", width = 12, height = 3, bufpos = { 100, 10 } }
		)

		-- See the note above regarding the duplication of this test code.
		local parent_popup = vim.api.nvim_get_current_win()
		local parent_buffer = vim.api.nvim_get_current_buf()
		assert.True(util.is_floating(parent_popup))
		local parent_config = vim.api.nvim_win_get_config(parent_popup)

		local child_popup = assert(detour.Detour())
		assert.True(util.is_floating(child_popup))
		local child_config = vim.api.nvim_win_get_config(child_popup)
		local child_buffer = vim.api.nvim_get_current_buf()
		-- The nested popup should always be on top of the parent popup
		assert.True(child_config.zindex > parent_config.zindex)

		assert.same(#vim.api.nvim_list_wins(), 3)
		assert.same(base_buffer, parent_buffer)
		assert.same(parent_buffer, child_buffer)

		vim.cmd.quit()
		assert.same(#vim.api.nvim_list_wins(), 2)
		vim.cmd.quit()
		assert.same(#vim.api.nvim_list_wins(), 1)
	end)

	-- TODO: Make sure popups are fully contained within their parents
	it("create nested popup", function()
		local base_buffer = vim.api.nvim_get_current_buf()

		local parent_popup = assert(detour.Detour())
		local parent_buffer = vim.api.nvim_get_current_buf()
		assert.True(util.is_floating(parent_popup))
		local parent_config = vim.api.nvim_win_get_config(parent_popup)

		local child_popup = assert(detour.Detour())
		assert.True(util.is_floating(child_popup))
		local child_config = vim.api.nvim_win_get_config(child_popup)
		local child_buffer = vim.api.nvim_get_current_buf()
		-- The nested popup should always be on top of the parent popup
		assert.True(child_config.zindex > parent_config.zindex)

		assert.same(#vim.api.nvim_list_wins(), 3)
		assert.same(base_buffer, parent_buffer)
		assert.same(parent_buffer, child_buffer)

		vim.cmd.quit()
		assert.same(#vim.api.nvim_list_wins(), 2)
		vim.cmd.quit()
		assert.same(#vim.api.nvim_list_wins(), 1)
	end)

	it("react to a coverable window closing", function()
		vim.cmd.wincmd("v")
		local coverable_window = vim.api.nvim_get_current_win()
		local popup = assert(detour.Detour())
		assert.True(util.overlap(popup, coverable_window))
		vim.fn.win_gotoid(coverable_window)
		vim.cmd.wincmd("s")
		local uncoverable_win = vim.api.nvim_get_current_win()
		vim.api.nvim_exec_autocmds(
			"WinResized",
			{ data = { windows = { coverable_window } } }
		)
		assert.False(util.overlap(popup, uncoverable_win))
		vim.api.nvim_win_close(coverable_window, true)

		assert.False(util.overlap(popup, uncoverable_win))
		vim.api.nvim_win_close(uncoverable_win, true)
	end)

	it("create popup over current window", function()
		local window_a = vim.api.nvim_get_current_win()
		vim.cmd.wincmd("s")
		local window_b = vim.api.nvim_get_current_win()
		local popup_b = assert(detour.DetourCurrentWindow())
		assert.False(util.overlap(window_a, popup_b))
		assert.True(util.overlap(window_b, popup_b))
		vim.fn.win_gotoid(window_a)
		local popup_a = assert(detour.Detour())
		assert.True(util.overlap(window_a, popup_a))
		assert.False(util.overlap(window_b, popup_a))
	end)

	it("Do not allow two popups over the same window", function()
		local win = vim.api.nvim_get_current_win()
		local popup = assert(detour.Detour())
		vim.fn.win_gotoid(win)
		assert.Nil(detour.Detour())
		assert.same(Set({ win, popup }), Set(vim.api.nvim_tabpage_list_wins(0)))

		vim.fn.win_gotoid(win)
		assert.Nil(detour.DetourCurrentWindow())
		assert.same(Set({ win, popup }), Set(vim.api.nvim_tabpage_list_wins(0)))
	end)

	it(
		"Do not allow two 'current window' popups over the same window",
		function()
			local win = vim.api.nvim_get_current_win()
			local popup = assert(detour.DetourCurrentWindow())
			vim.fn.win_gotoid(win)
			assert.Nil(detour.DetourCurrentWindow())
			assert.same(
				Set({ win, popup }),
				Set(vim.api.nvim_tabpage_list_wins(0))
			)

			vim.fn.win_gotoid(win)
			assert.Nil(detour.Detour())
			assert.same(
				Set({ win, popup }),
				Set(vim.api.nvim_tabpage_list_wins(0))
			)
		end
	)

	it("Switch focus to a popup's floating parent when it's closed", function()
		vim.api.nvim_open_win(
			vim.api.nvim_win_get_buf(0),
			true,
			{ relative = "win", width = 12, height = 3, bufpos = { 100, 10 } }
		)

		local wins = { vim.api.nvim_get_current_win() }
		for _ = 1, 10 do
			table.insert(wins, detour.Detour())
			for j, win in ipairs(wins) do
				assert(win)
				assert.same(
					vim.api.nvim_win_get_config(win).focusable,
					j == #wins
				)
			end
		end

		for _ = 1, 10 do
			table.remove(wins, #wins)
			vim.cmd.close()
			assert.same(vim.api.nvim_get_current_win(), wins[#wins])
			for j, win in ipairs(wins) do
				assert.same(
					vim.api.nvim_win_get_config(win).focusable,
					j == #wins
				)
			end
		end
	end)

	it("Switch focus to a popup's parent when it's closed", function()
		local wins = { vim.api.nvim_get_current_win() }
		for _ = 1, 10 do
			table.insert(wins, detour.Detour())
			for j, win in ipairs(wins) do
				assert(win)
				if j > 1 then -- the base window cannot be unfocusable
					assert.same(
						vim.api.nvim_win_get_config(win).focusable,
						j == #wins
					)
				end
			end
		end

		for _ = 1, 10 do
			table.remove(wins, #wins)
			vim.cmd.close()
			assert.same(vim.api.nvim_get_current_win(), wins[#wins])
			for j, win in ipairs(wins) do
				if j > 1 then -- the base window cannot be unfocusable
					assert.same(
						vim.api.nvim_win_get_config(win).focusable,
						j == #wins
					)
				end
			end
		end
	end)

	it(
		"Handle cases when popups close without throwing a WinClosed event",
		function()
			local popup = assert(detour.DetourCurrentWindow())
			vim.api.nvim_create_autocmd({ "WinLeave" }, {
				callback = function()
					vim.api.nvim_win_close(0, true)
					return true
				end,
			})
			vim.cmd.wincmd("h") -- Close popup without WinClosed event
			assert.False(util.is_open(popup))
			assert(detour.DetourCurrentWindow())
		end
	)

	it("Test CloseOnLeave", function()
		local popup_id = assert(detour.Detour())
		features.CloseOnLeave(popup_id)
		assert.True(util.is_open(popup_id))
		vim.cmd.wincmd("w")
		assert.False(util.is_open(popup_id))

		popup_id = assert(detour.Detour())
		features.CloseOnLeave(popup_id)
		assert.True(util.is_open(popup_id))
		vim.cmd.split()
		assert.False(util.is_open(popup_id))
	end)

	it("Test ShowPathInTitle", function()
		vim.cmd.file("/tmp/detour_test_a")
		local popup_id = assert(detour.Detour())
		assert.same(
			"/tmp/detour_test_a",
			vim.api.nvim_win_get_config(0).title[1][1]
		)
		vim.cmd.file("/tmp/detour_test_b")
		-- Simulate the event triggered by the `on_win` decorator
		vim.cmd.doautocmd("User DetourUpdateTitle" .. util.stringify(popup_id))
		assert.same(
			"/tmp/detour_test_b",
			vim.api.nvim_win_get_config(0).title[1][1]
		)
	end)
end)


================================================
FILE: spec/detour_uncover_spec.lua
================================================
local detour = require("detour")
local util = require("detour.util")
local features = require("detour.features")

local function Set(list)
  local set = {}
  for _, l in ipairs(list) do
    set[l] = true
  end
  return set
end

describe("detour uncover feature", function()
  before_each(function()
    vim.g.detour_testing = true
    vim.cmd([[ 
      %bwipeout!
      mapclear
      nmapclear
      vmapclear
      xmapclear
      smapclear
      omapclear
      mapclear
      imapclear
      lmapclear
      cmapclear
      tmapclear
    ]])
    vim.api.nvim_clear_autocmds({}) -- delete any autocmds not in a group
    for _, autocmd in ipairs(vim.api.nvim_get_autocmds({ pattern = "*" })) do
      if vim.startswith(autocmd.group_name, "detour-") then
        vim.api.nvim_del_autocmd(autocmd.id)
      end
    end
    vim.o.splitbelow = true
    vim.o.splitright = true
  end)

  it("uncover one base window and resize popup accordingly", function()
    -- Create a simple 2-column layout
    local left_base = vim.api.nvim_get_current_win()
    vim.cmd.vsplit()
    local right_base = vim.api.nvim_get_current_win()

    -- Create a detour covering both base windows
    local popup = assert(detour.Detour())

    -- Sanity: popup overlaps both bases initially
    assert.True(util.overlap(popup, left_base))
    assert.True(util.overlap(popup, right_base))

    -- Uncover the left base window
    assert.True(features.UncoverWindow(left_base))

    -- The popup should no longer overlap the left base
    assert.False(util.overlap(popup, left_base))
    -- The popup should still overlap the right base
    assert.True(util.overlap(popup, right_base))

    -- Covered windows should only include the right base now
    local covered = Set(util.find_covered_windows(popup))
    assert.True(covered[right_base])
    assert.is_nil(covered[left_base])
  end)

  it("prevent uncovering the last remaining base window", function()
    -- Create a simple 2-column layout
    local left_base = vim.api.nvim_get_current_win()
    vim.cmd.vsplit()
    local right_base = vim.api.nvim_get_current_win()

    -- Create a detour covering both base windows
    local popup = assert(detour.Detour())

    -- Uncover one window first
    assert.True(features.UncoverWindow(left_base))

    -- Attempt to uncover the last remaining window should fail
    assert.False(features.UncoverWindow(right_base))

    -- State should remain unchanged: popup still overlaps right_base
    assert.True(util.overlap(popup, right_base))
    assert.False(util.overlap(popup, left_base))
  end)
end)



================================================
FILE: spec/internal_spec.lua
================================================
local detour = require("detour")
local internal = require("detour.internal")
local util = require("detour.util")

describe("detour internal", function()
	before_each(function()
		vim.g.detour_testing = true
		vim.cmd([[ 
      %bwipeout!
      mapclear
      nmapclear
      vmapclear
      xmapclear
      smapclear
      omapclear
      mapclear
      imapclear
      lmapclear
      cmapclear
      tmapclear
    ]])
		vim.api.nvim_clear_autocmds({}) -- delete any autocmds not in a group
		for _, autocmd in ipairs(vim.api.nvim_get_autocmds({})) do
			if vim.startswith(autocmd.group_name, "detour-") then
				vim.api.nvim_del_autocmd(autocmd.id)
			end
		end
		vim.o.splitbelow = true
		vim.o.splitright = true
	end)

	it("teardown is idempotent and clears reservations", function()
		local popup = assert(detour.Detour())
		internal.teardown_detour(popup)
		internal.teardown_detour(popup)
		assert.is_nil(internal.get_reserved_windows(popup))
	end)

	it("garbage_collect removes closed popups", function()
		local popup = assert(detour.Detour())
		vim.api.nvim_win_close(popup, true)
		internal.garbage_collect()
		assert.False(vim.tbl_contains(internal.list_popups(), popup))
		assert.is_nil(internal.get_reserved_windows(popup))
	end)

	it("get_reserved_windows filters out closed windows", function()
		vim.cmd.vsplit()
		local right = vim.api.nvim_get_current_win()

		local popup = assert(detour.Detour())
		assert.True(util.overlap(popup, right))

		-- Close one base and ensure it is removed from reservations
		vim.fn.win_gotoid(right)
		vim.api.nvim_win_close(right, true)
		local reserved = internal.get_reserved_windows(popup)
		assert.truthy(reserved)
		assert.False(vim.tbl_contains(reserved, right))
	end)
end)


================================================
FILE: spec/util_spec.lua
================================================
local util = require("detour.util")

describe("detour util", function()
	before_each(function()
		vim.g.detour_testing = true
		vim.cmd([[ 
      %bwipeout!
      mapclear
      nmapclear
      vmapclear
      xmapclear
      smapclear
      omapclear
      mapclear
      imapclear
      lmapclear
      cmapclear
      tmapclear
    ]])
		for _, autocmd in ipairs(vim.api.nvim_get_autocmds({})) do
			if vim.startswith(autocmd.group_name, "detour-") then
				vim.api.nvim_del_autocmd(autocmd.id)
			end
		end
		vim.o.splitbelow = true
		vim.o.splitright = true
	end)

	it("pairs_by_keys iterates keys in order", function()
		local t = { c = 3, a = 1, b = 2 }
		local keys = {}
		for k in util.pairs_by_keys(t) do
			table.insert(keys, k)
		end
		assert.same({ "a", "b", "c" }, keys)
	end)

	it("stringify maps digits to letters", function()
		assert.same("bc", util.stringify(12))
		assert.same("bcd", util.stringify(123))
	end)

	it("base_at_screenpos finds base window by coordinates", function()
		local tab = vim.api.nvim_get_current_tabpage()
		-- two columns layout
		local left = vim.api.nvim_get_current_win()
		vim.cmd.vsplit()
		local right = vim.api.nvim_get_current_win()

		local function inside(win)
			local top, _, leftx, _ = util.get_text_area_dimensions(win)
			return top + 1, leftx + 1
		end

		local r, c = inside(left)
		assert.same(left, util.base_at_screenpos(tab, r, c))

		r, c = inside(right)
		assert.same(right, util.base_at_screenpos(tab, r, c))
	end)
end)


================================================
FILE: spec/windowing_algorithm_spec.lua
================================================
local algo = require("detour.windowing_algorithm")
local util = require("detour.util")

describe("detour windowing_algorithm", function()
  before_each(function()
    vim.g.detour_testing = true
    vim.cmd([[ 
      %bwipeout!
      mapclear
      nmapclear
      vmapclear
      xmapclear
      smapclear
      omapclear
      mapclear
      imapclear
      lmapclear
      cmapclear
      tmapclear
    ]])
    vim.api.nvim_clear_autocmds({})
    vim.o.splitbelow = true
    vim.o.splitright = true
  end)

  it("construct_nest returns inner rectangle inside parent float", function()
    -- create a parent float with known geometry and no border
    local parent = vim.api.nvim_open_win(vim.api.nvim_get_current_buf(), true, {
      relative = "editor",
      row = 5,
      col = 10,
      width = 30,
      height = 10,
      border = "none",
      zindex = 1,
    })
    assert.truthy(parent)

    local top, bottom, left, right = util.get_text_area_dimensions(parent)
    local expected_width = (right - left)
    local expected_height = (bottom - top)
    if expected_height >= 3 then
      expected_height = expected_height - 2
      top = top + 1
    end
    if expected_width >= 3 then
      expected_width = expected_width - 2
      left = left + 1
    end

    local opts = algo.construct_nest(parent, 2)
    assert.same({
      relative = "editor",
      row = top,
      col = left,
      width = expected_width,
      height = expected_height,
      border = "rounded",
      zindex = 2,
    }, opts)
  end)
end)

Download .txt
gitextract_mob2na4m/

├── .busted
├── .github/
│   └── workflows/
│       └── release.yml
├── .gitignore
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── detour.nvim-scm-1.rockspec
├── doc/
│   ├── detour.txt
│   └── tags
├── examples/
│   ├── help-float.md
│   └── telescope.md
├── lua/
│   └── detour/
│       ├── algorithm_doc.lua
│       ├── config.lua
│       ├── features.lua
│       ├── init.lua
│       ├── internal.lua
│       ├── movements.lua
│       ├── show_path_in_title.lua
│       ├── util.lua
│       └── windowing_algorithm.lua
├── plugin/
│   └── detour.lua
├── run-in-docker.sh
└── spec/
    ├── config_spec.lua
    ├── detour_auto_unreserve_spec.lua
    ├── detour_close_stack_spec.lua
    ├── detour_hide_reveal_spec.lua
    ├── detour_movements_spec.lua
    ├── detour_spec.lua
    ├── detour_uncover_spec.lua
    ├── internal_spec.lua
    ├── util_spec.lua
    └── windowing_algorithm_spec.lua
Condensed preview — 34 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (112K chars).
[
  {
    "path": ".busted",
    "chars": 165,
    "preview": "return {\n  _all = {\n    coverage = false,\n    lpath = \"lua/?.lua;lua/?/init.lua\",\n  },\n  default = {\n    verbose = true,"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 489,
    "preview": "name: LuaRocks release\non:\n  push:\n    tags: # Will upload to luarocks.org when a tag is pushed\n      - \"*\"\n  pull_reque"
  },
  {
    "path": ".gitignore",
    "chars": 429,
    "preview": "# 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*."
  },
  {
    "path": "CHANGELOG.md",
    "chars": 2296,
    "preview": "# Changelog\n\n## v2.0.0\n\n### Added\n\n* Features: Hide/reveal detours and uncover hidden base windows\n* Implement `CloseCur"
  },
  {
    "path": "Dockerfile",
    "chars": 1237,
    "preview": "FROM alpine:3.22\n\nENV LUA_MAJOR_VERSION 5.1\nENV LUA_MINOR_VERSION 5\nENV LUA_VERSION ${LUA_MAJOR_VERSION}.${LUA_MINOR_VER"
  },
  {
    "path": "LICENSE",
    "chars": 1066,
    "preview": "MIT License\n\nCopyright (c) 2023 Roger Kim\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
  },
  {
    "path": "Makefile",
    "chars": 1160,
    "preview": "DOCKER_IMAGE = detour_tester\n\n.PHONY : test\n\nimage: Dockerfile\n\tdocker build -t ${DOCKER_IMAGE} .\n\ttouch image\n\ntest: im"
  },
  {
    "path": "README.md",
    "chars": 8242,
    "preview": "# 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 yo"
  },
  {
    "path": "detour.nvim-scm-1.rockspec",
    "chars": 207,
    "preview": "rockspec_format = '3.0'\npackage = 'detour.nvim'\nversion = 'scm-1'\n\ntest_dependencies = {\n  'lua >= 5.1',\n}\n\nsource = {\n "
  },
  {
    "path": "doc/detour.txt",
    "chars": 9822,
    "preview": "==============================================================================\n                                         "
  },
  {
    "path": "doc/tags",
    "chars": 1300,
    "preview": "detour\tdetour.txt\t/*detour*\ndetour-algorithm\tdetour.txt\t/*detour-algorithm*\ndetour-features\tdetour.txt\t/*detour-features"
  },
  {
    "path": "examples/help-float.md",
    "chars": 384,
    "preview": "# Display help files in a Detour window\n\n```lua\nvim.keymap.set(\"n\", \"<A-h>\", function()\n\tlocal popup_id = require(\"detou"
  },
  {
    "path": "examples/telescope.md",
    "chars": 1452,
    "preview": "# Telescope keymaps\nHere are examples of useful keymaps where you use detour popups together with the [telescope plugin]"
  },
  {
    "path": "lua/detour/algorithm_doc.lua",
    "chars": 1259,
    "preview": "---@mod detour.algorithm\n---@tag detour-algorithm\n---@brief [[\n---Detour's windowing algorithm computes the largest rect"
  },
  {
    "path": "lua/detour/config.lua",
    "chars": 820,
    "preview": "---@mod detour.config\n---User configuration and defaults for detour.nvim.\n\n---@class detour.config.Options\n---@field tit"
  },
  {
    "path": "lua/detour/features.lua",
    "chars": 4999,
    "preview": "---@mod detour.features\n---@brief [[\n--- Optional detour.nvim features.\n---@brief ]]\n---@tag detour-features\n\nlocal feat"
  },
  {
    "path": "lua/detour/init.lua",
    "chars": 10645,
    "preview": "---@mod detour\n---@tag detour-into\n---@brief [[\n---Neovim/Vim's floating windows are a great utility to use in plugins a"
  },
  {
    "path": "lua/detour/internal.lua",
    "chars": 5660,
    "preview": "-- DO NOT DEPEND ON THIS FILE!\n-- This is an \"internal\" file and can have breaking changes without warning.\n\n---@mod det"
  },
  {
    "path": "lua/detour/movements.lua",
    "chars": 7308,
    "preview": "---@mod detour.movements\n---@brief [[\n---Detour.nvim breaks window navigation commands such as\n---`<C-w>w`, `<C-w>j`, or"
  },
  {
    "path": "lua/detour/show_path_in_title.lua",
    "chars": 1968,
    "preview": "---@mod detour.show_path_in_title\n---Internal helper for updating popup titles.\n\n-- Put this code in its own library to "
  },
  {
    "path": "lua/detour/util.lua",
    "chars": 8713,
    "preview": "---@mod detour.util\n---Utilities for window geometry, state checks, and helpers.\n\nlocal util = {}\n\n---@class detour.util"
  },
  {
    "path": "lua/detour/windowing_algorithm.lua",
    "chars": 5224,
    "preview": "---@mod detour.windowing_algorithm\n---Layout algorithm for computing popup positions.\n\nlocal algo = {}\n\nlocal util = req"
  },
  {
    "path": "plugin/detour.lua",
    "chars": 628,
    "preview": "local detour = require(\"detour\")\nlocal features = require(\"detour.features\")\n\nvim.api.nvim_create_user_command(\"Detour\","
  },
  {
    "path": "run-in-docker.sh",
    "chars": 210,
    "preview": "#!/bin/sh\nnvim -u NONE \\\n  -c \"lua local k,l,_=pcall(require,'luarocks.loader') _=k and l.add_context('busted','$BUSTED_"
  },
  {
    "path": "spec/config_spec.lua",
    "chars": 1218,
    "preview": "local detour = require(\"detour\")\nlocal config = require(\"detour.config\")\n\ndescribe(\"detour config\", function()\n\tbefore_e"
  },
  {
    "path": "spec/detour_auto_unreserve_spec.lua",
    "chars": 1445,
    "preview": "local detour = require(\"detour\")\nlocal util = require(\"detour.util\")\n\ndescribe(\"detour auto-unreserve on interaction\", f"
  },
  {
    "path": "spec/detour_close_stack_spec.lua",
    "chars": 1913,
    "preview": "local detour = require(\"detour\")\nlocal util = require(\"detour.util\")\nlocal features = require(\"detour.features\")\n\ndescri"
  },
  {
    "path": "spec/detour_hide_reveal_spec.lua",
    "chars": 1778,
    "preview": "local detour = require(\"detour\")\nlocal util = require(\"detour.util\")\nlocal features = require(\"detour.features\")\n\ndescri"
  },
  {
    "path": "spec/detour_movements_spec.lua",
    "chars": 5618,
    "preview": "local detour = require(\"detour\")\nlocal movements = require(\"detour.movements\")\nlocal util = require(\"detour.util\")\n\nfunc"
  },
  {
    "path": "spec/detour_spec.lua",
    "chars": 8354,
    "preview": "local detour = require(\"detour\")\nlocal util = require(\"detour.util\")\nlocal features = require(\"detour.features\")\n\nfuncti"
  },
  {
    "path": "spec/detour_uncover_spec.lua",
    "chars": 2579,
    "preview": "local detour = require(\"detour\")\nlocal util = require(\"detour.util\")\nlocal features = require(\"detour.features\")\n\nlocal "
  },
  {
    "path": "spec/internal_spec.lua",
    "chars": 1732,
    "preview": "local detour = require(\"detour\")\nlocal internal = require(\"detour.internal\")\nlocal util = require(\"detour.util\")\n\ndescri"
  },
  {
    "path": "spec/util_spec.lua",
    "chars": 1489,
    "preview": "local util = require(\"detour.util\")\n\ndescribe(\"detour util\", function()\n\tbefore_each(function()\n\t\tvim.g.detour_testing ="
  },
  {
    "path": "spec/windowing_algorithm_spec.lua",
    "chars": 1532,
    "preview": "local algo = require(\"detour.windowing_algorithm\")\nlocal util = require(\"detour.util\")\n\ndescribe(\"detour windowing_algor"
  }
]

About this extraction

This page contains the full source code of the carbon-steel/detour.nvim GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 34 files (100.9 KB), approximately 28.3k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!