├── .github
├── FUNDING.yml
├── configs
│ └── commitlint.config.js
└── workflows
│ ├── commit-messages.yaml
│ ├── lines.yaml
│ ├── luacheck.yml
│ ├── panvimdoc.yml
│ └── release.yml
├── .gitignore
├── .luacheckrc
├── .luarc.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc
└── cokeline.txt
├── lua
├── cokeline
│ ├── augroups.lua
│ ├── buffers.lua
│ ├── components.lua
│ ├── config.lua
│ ├── context.lua
│ ├── handlers.lua
│ ├── history.lua
│ ├── hlgroups.lua
│ ├── hover.lua
│ ├── init.lua
│ ├── lazy.lua
│ ├── mappings.lua
│ ├── rendering.lua
│ ├── rhs.lua
│ ├── sidebar.lua
│ ├── sliders.lua
│ ├── state.lua
│ ├── tabs.lua
│ └── utils.lua
└── resession
│ └── extensions
│ └── cokeline.lua
├── neovim.yml
├── selene.toml
└── stylua.toml
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: [ "https://paypal.me/noib3", "https://paypal.me/willothy" ]
2 |
--------------------------------------------------------------------------------
/.github/configs/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ["@commitlint/config-conventional"] };
2 |
--------------------------------------------------------------------------------
/.github/workflows/commit-messages.yaml:
--------------------------------------------------------------------------------
1 | name: "Commit message check"
2 | on: [push, pull_request]
3 | jobs:
4 | check-commit-message:
5 | name: Ensure messages conform to Conventional Commits standard
6 | runs-on: ubuntu-22.04
7 | steps:
8 | - uses: actions/checkout@v3
9 | with:
10 | fetch-depth: 0
11 | - name: Install required dependencies
12 | run: |
13 | sudo apt update
14 | sudo apt install -y git curl
15 | curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
16 | sudo DEBIAN_FRONTEND=noninteractive apt install -y nodejs
17 | - name: Print versions
18 | run: |
19 | git --version
20 | node --version
21 | npm --version
22 | npx commitlint --version
23 | - name: Install commitlint
24 | run: |
25 | npm install conventional-changelog-conventionalcommits
26 | npm install commitlint@latest
27 | npm install @commitlint/config-conventional
28 |
29 | - name: Validate current commit (last commit) with commitlint
30 | if: github.event_name == 'push'
31 | run: npx commitlint --config ".github/configs/commitlint.config.js" --from HEAD~1 --to HEAD --verbose
32 |
33 | - name: Validate PR commits with commitlint
34 | if: github.event_name == 'pull_request'
35 | run: npx commitlint --config ".github/configs/commitlint.config.js" --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose
36 |
--------------------------------------------------------------------------------
/.github/workflows/lines.yaml:
--------------------------------------------------------------------------------
1 | name: "Check line width"
2 | on: [push, pull_request]
3 | jobs:
4 | # line-number:
5 | # name: Sloc < 1000
6 | # runs-on: ubuntu-latest
7 | # steps:
8 | # - uses: actions/checkout@v2
9 | # - run: cargo install tokei
10 | # - run: num_sloc=$(tokei --type=Lua,'Vim script' --output=json . | jq .Total.code); if [ ${num_sloc} -gt 999 ]; then exit 1; fi
11 |
12 | line-width:
13 | name: Line width < 80 characters
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v2
17 | - run: cargo install --locked ripgrep
18 | - run: max_line_width=$(rg --glob '*.{lua,vim}' --no-heading --no-filename --no-line-number . | awk '{print length}' | sort -n | tail -n 1); if [ ${max_line_width} -gt 79 ]; then exit 1; fi
19 |
--------------------------------------------------------------------------------
/.github/workflows/luacheck.yml:
--------------------------------------------------------------------------------
1 | name: Luacheck
2 |
3 | on:
4 | push:
5 | branches: [ "main", "master" ]
6 | pull_request:
7 | branches: [ "main", "master" ]
8 |
9 | workflow_dispatch:
10 |
11 |
12 | jobs:
13 | luacheck:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v3
18 | - name: Luacheck
19 | uses: lunarmodules/luacheck@v1.1.0
20 |
--------------------------------------------------------------------------------
/.github/workflows/panvimdoc.yml:
--------------------------------------------------------------------------------
1 | name: panvimdoc
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | workflow_dispatch:
7 |
8 | permissions:
9 | contents: write
10 | pull-requests: write
11 |
12 | jobs:
13 | docs:
14 | runs-on: ubuntu-latest
15 | name: pandoc to vimdoc
16 | steps:
17 | - uses: actions/checkout@v2
18 | - uses: kdheepak/panvimdoc@main
19 | with:
20 | vimdoc: cokeline
21 | version: "NVIM >=0.7"
22 | toc: true
23 | demojify: true
24 | treesitter: true
25 | - uses: stefanzweifel/git-auto-commit-action@v4
26 | with:
27 | commit_message: "chore(docs): autogenerate vimdoc"
28 | branch: ${{ github.head_ref }}
29 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: write
10 | pull-requests: write
11 |
12 | jobs:
13 | release-please:
14 | runs-on: ubuntu-22.04
15 | steps:
16 | - uses: google-github-actions/release-please-action@v3
17 | with:
18 | release-type: simple
19 | package-name: nvim-cokeline
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /doc/tags
2 | /TODO.md
3 |
--------------------------------------------------------------------------------
/.luacheckrc:
--------------------------------------------------------------------------------
1 | ignore = {
2 | "631", -- max_line_length
3 | --"212/_.*", -- unused argument, for vars with "_" prefix
4 | "212", -- Unused argument, In the case of callback function, _arg_name is easier to understand than _, so this option is set to off.
5 | "121", -- setting read-only global variable 'vim'
6 | "122", -- setting read-only field of global variable 'vim'
7 | }
8 |
9 | -- Global objects defined by the C code
10 | read_globals = {
11 | "vim",
12 | }
13 |
14 | globals = {
15 | "vim.g",
16 | "vim.b",
17 | "vim.w",
18 | "vim.o",
19 | "vim.bo",
20 | "vim.wo",
21 | "vim.go",
22 | "vim.env"
23 | }
24 |
--------------------------------------------------------------------------------
/.luarc.json:
--------------------------------------------------------------------------------
1 | {
2 | "workspace.checkThirdParty": false
3 | }
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [Unreleased](https://github.com/willothy/nvim-cokeline/tree/HEAD)
4 |
5 | [Full Changelog](https://github.com/willothy/nvim-cokeline/compare/v0.4.0...HEAD)
6 |
7 | **Merged pull requests:**
8 |
9 | - refactor: use modules, lazy requires [\#157](https://github.com/willothy/nvim-cokeline/pull/157) ([willothy](https://github.com/willothy))
10 | - fixup: \(3682c78e\): README: Replace `focused` with `fg` in default_hl [\#152](https://github.com/willothy/nvim-cokeline/pull/152) ([UtsavBalar1231](https://github.com/UtsavBalar1231))
11 | - feat!: support for more highlight attrs [\#150](https://github.com/willothy/nvim-cokeline/pull/150) ([willothy](https://github.com/willothy))
12 | - Update README.md [\#146](https://github.com/willothy/nvim-cokeline/pull/146) ([sashalikesplanes](https://github.com/sashalikesplanes))
13 | - feat: custom sorting functions [\#145](https://github.com/willothy/nvim-cokeline/pull/145) ([willothy](https://github.com/willothy))
14 | - feat: provide require\("cokeline.sidebar"\).get_width\(\) and cache sidebar widths [\#143](https://github.com/willothy/nvim-cokeline/pull/143) ([willothy](https://github.com/willothy))
15 | - refactor!: make scratch buffer if deleting last buffer [\#142](https://github.com/willothy/nvim-cokeline/pull/142) ([willothy](https://github.com/willothy))
16 | - feat: global \(per buffer/tab/etc\) hover events [\#140](https://github.com/willothy/nvim-cokeline/pull/140) ([willothy](https://github.com/willothy))
17 |
18 | ## [v0.4.0](https://github.com/willothy/nvim-cokeline/tree/v0.4.0) (2023-07-02)
19 |
20 | [Full Changelog](https://github.com/willothy/nvim-cokeline/compare/v0.3.0...v0.4.0)
21 |
22 | **Merged pull requests:**
23 |
24 | - feat: sidebars on rhs of editor [\#138](https://github.com/willothy/nvim-cokeline/pull/138) ([willothy](https://github.com/willothy))
25 | - feat: native tab support [\#136](https://github.com/willothy/nvim-cokeline/pull/136) ([willothy](https://github.com/willothy))
26 | - perf: use stream-based iterators where possible [\#135](https://github.com/willothy/nvim-cokeline/pull/135) ([willothy](https://github.com/willothy))
27 | - Use named color names [\#134](https://github.com/willothy/nvim-cokeline/pull/134) ([lucassperez](https://github.com/lucassperez))
28 | - feat\(pick\): retain pick char order when not using filename [\#132](https://github.com/willothy/nvim-cokeline/pull/132) ([willothy](https://github.com/willothy))
29 | - feat: buffer history ringbuffer [\#131](https://github.com/willothy/nvim-cokeline/pull/131) ([willothy](https://github.com/willothy))
30 | - feat: support for passing highlight group names [\#130](https://github.com/willothy/nvim-cokeline/pull/130) ([nenikitov](https://github.com/nenikitov))
31 | - feat: allow multiple vert splits in sidebar [\#129](https://github.com/willothy/nvim-cokeline/pull/129) ([willothy](https://github.com/willothy))
32 | - fix: allow multibyte characters on buffer picker [\#123](https://github.com/willothy/nvim-cokeline/pull/123) ([lucassperez](https://github.com/lucassperez))
33 | - feat: add fallback icon by filetype [\#121](https://github.com/willothy/nvim-cokeline/pull/121) ([Webblitchy](https://github.com/Webblitchy))
34 | - fix: index update value [\#118](https://github.com/willothy/nvim-cokeline/pull/118) ([Equilibris](https://github.com/Equilibris))
35 | - feat: add fill_hl config option [\#114](https://github.com/willothy/nvim-cokeline/pull/114) ([FollieHiyuki](https://github.com/FollieHiyuki))
36 | - feat: Rearrange buffers with mouse drag [\#113](https://github.com/willothy/nvim-cokeline/pull/113) ([willothy](https://github.com/willothy))
37 | - feat\(pick\): release taken letters when deleting buffers [\#112](https://github.com/willothy/nvim-cokeline/pull/112) ([soifou](https://github.com/soifou))
38 | - feat: hover events [\#111](https://github.com/willothy/nvim-cokeline/pull/111) ([willothy](https://github.com/willothy))
39 | - feat: per-component click handlers [\#106](https://github.com/willothy/nvim-cokeline/pull/106) ([willothy](https://github.com/willothy))
40 | - feat: close by step and by index [\#104](https://github.com/willothy/nvim-cokeline/pull/104) ([willothy](https://github.com/willothy))
41 | - feat: handle click events with Lua, add bufdelete util [\#103](https://github.com/willothy/nvim-cokeline/pull/103) ([willothy](https://github.com/willothy))
42 | - Proposal: Add sorting by directory [\#97](https://github.com/willothy/nvim-cokeline/pull/97) ([ewok](https://github.com/ewok))
43 | - feat: `pick.use_filename` and `pick.letters` config options [\#88](https://github.com/willothy/nvim-cokeline/pull/88) ([ProspectPyxis](https://github.com/ProspectPyxis))
44 | - fix\(mapping\): handle gracefully keyboard interrupt on buffer pick. [\#81](https://github.com/willothy/nvim-cokeline/pull/81) ([soifou](https://github.com/soifou))
45 | - Update README.md [\#70](https://github.com/willothy/nvim-cokeline/pull/70) ([crivotz](https://github.com/crivotz))
46 | - Adding is_last and is_first to buffer return [\#69](https://github.com/willothy/nvim-cokeline/pull/69) ([miversen33](https://github.com/miversen33))
47 | - Add new configuration [\#68](https://github.com/willothy/nvim-cokeline/pull/68) ([danielnieto](https://github.com/danielnieto))
48 | - fix: check for Windows always fails when computing `unique_prefix` [\#65](https://github.com/willothy/nvim-cokeline/pull/65) ([EtiamNullam](https://github.com/EtiamNullam))
49 | - fix: cokeline-pick-close and cokeline-pick-focus in v0.7 [\#53](https://github.com/willothy/nvim-cokeline/pull/53) ([matt-riley](https://github.com/matt-riley))
50 |
51 | ## [v0.3.0](https://github.com/willothy/nvim-cokeline/tree/v0.3.0) (2022-03-06)
52 |
53 | [Full Changelog](https://github.com/willothy/nvim-cokeline/compare/v0.2.0...v0.3.0)
54 |
55 | **Merged pull requests:**
56 |
57 | - fix: Allow focusing a buffer from a non-valid buffer [\#40](https://github.com/willothy/nvim-cokeline/pull/40) ([tamirzb](https://github.com/tamirzb))
58 |
59 | ## [v0.2.0](https://github.com/willothy/nvim-cokeline/tree/v0.2.0) (2022-01-01)
60 |
61 | [Full Changelog](https://github.com/willothy/nvim-cokeline/compare/v0.1.0...v0.2.0)
62 |
63 | ## [v0.1.0](https://github.com/willothy/nvim-cokeline/tree/v0.1.0) (2021-12-07)
64 |
65 | [Full Changelog](https://github.com/willothy/nvim-cokeline/compare/68b23cb77e2bf76df92a8043612e655e04507ed6...v0.1.0)
66 |
67 | **Merged pull requests:**
68 |
69 | - Update README.md [\#27](https://github.com/willothy/nvim-cokeline/pull/27) ([KadoBOT](https://github.com/KadoBOT))
70 | - Add author links to readme [\#20](https://github.com/willothy/nvim-cokeline/pull/20) ([alex-popov-tech](https://github.com/alex-popov-tech))
71 | - fix: correct error in readme.md [\#7](https://github.com/willothy/nvim-cokeline/pull/7) ([olimorris](https://github.com/olimorris))
72 | - :bug: fix: `unique_prefix` on windows [\#3](https://github.com/willothy/nvim-cokeline/pull/3) ([Neelfrost](https://github.com/Neelfrost))
73 | - :bug: fix: use correct path separators for unique_prefix depending on OS [\#2](https://github.com/willothy/nvim-cokeline/pull/2) ([Neelfrost](https://github.com/Neelfrost))
74 |
75 | \* _This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)_
76 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Riccardo Mazzarini
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | 👃 nvim-cokeline
4 |
5 |
6 |
7 |
8 | A Neovim bufferline for people with addictive personalities
9 |
10 |
11 |
12 | The goal of this plugin is not to be an opinionated bufferline with (more or
13 | less) limited customization options. Rather, it tries to provide a general
14 | framework allowing you to build **_your_** ideal bufferline, whatever that
15 | might look like.
16 |
17 |
18 | 
19 |
20 | ## :book: Table of Contents
21 |
22 | - [Features](#sparkles-features)
23 | - [Requirements](#electric_plug-requirements)
24 | - [Installation](#package-installation)
25 | - [Configuration](#wrench-configuration)
26 | - [Mappings](#musical_keyboard-mappings)
27 |
28 |
29 |
30 | ## :sparkles: Features
31 |
32 | ### Endlessly customizable
33 |
34 | `nvim-cokeline` aims to be the most customizable bufferline plugin around. If
35 | you have an idea in mind of what your bufferline should look like, you should
36 | be able to make it look that way. If you can't, open an issue and we'll try to
37 | make it happen!
38 |
39 |
40 | Here's a (very limited) showcase of what it can be configured to look like
41 | (check out the wiki for more examples):
42 |
43 |
44 | Click to see configuration
45 |
46 | ```lua
47 | local get_hex = require('cokeline.hlgroups').get_hl_attr
48 |
49 | require('cokeline').setup({
50 | default_hl = {
51 | fg = function(buffer)
52 | return
53 | buffer.is_focused
54 | and get_hex('ColorColumn', 'bg')
55 | or get_hex('Normal', 'fg')
56 | end,
57 | bg = function(buffer)
58 | return
59 | buffer.is_focused
60 | and get_hex('Normal', 'fg')
61 | or get_hex('ColorColumn', 'bg')
62 | end,
63 | },
64 |
65 | components = {
66 | {
67 | text = function(buffer) return ' ' .. buffer.devicon.icon end,
68 | fg = function(buffer) return buffer.devicon.color end,
69 | },
70 | {
71 | text = function(buffer) return buffer.unique_prefix end,
72 | fg = get_hex('Comment', 'fg'),
73 | italic = true
74 | },
75 | {
76 | text = function(buffer) return buffer.filename .. ' ' end,
77 | underline = function(buffer)
78 | return buffer.is_hovered and not buffer.is_focused
79 | end
80 | },
81 | {
82 | text = '',
83 | on_click = function(_, _, _, _, buffer)
84 | buffer:delete()
85 | end
86 | },
87 | {
88 | text = ' ',
89 | }
90 | },
91 | })
92 | ```
93 |
94 |
95 |
96 | 
97 |
98 |
99 | Click to see configuration
100 |
101 | ```lua
102 | local get_hex = require('cokeline.hlgroups').get_hl_attr
103 |
104 | local green = vim.g.terminal_color_2
105 | local yellow = vim.g.terminal_color_3
106 |
107 | require('cokeline').setup({
108 | default_hl = {
109 | fg = function(buffer)
110 | return
111 | buffer.is_focused
112 | and get_hex('Normal', 'fg')
113 | or get_hex('Comment', 'fg')
114 | end,
115 | bg = get_hex('ColorColumn', 'bg'),
116 | },
117 |
118 | components = {
119 | {
120 | text = '|',
121 | fg = function(buffer)
122 | return
123 | buffer.is_modified and yellow or green
124 | end
125 | },
126 | {
127 | text = function(buffer) return buffer.devicon.icon .. ' ' end,
128 | fg = function(buffer) return buffer.devicon.color end,
129 | },
130 | {
131 | text = function(buffer) return buffer.index .. ': ' end,
132 | },
133 | {
134 | text = function(buffer) return buffer.unique_prefix end,
135 | fg = get_hex('Comment', 'fg'),
136 | italic = true,
137 | },
138 | {
139 | text = function(buffer) return buffer.filename .. ' ' end,
140 | bold = function(buffer) return buffer.is_focused end,
141 | },
142 | {
143 | text = ' ',
144 | },
145 | },
146 | })
147 | ```
148 |
149 |
150 |
151 | 
152 |
153 |
154 | Click to see configuration
155 |
156 | ```lua
157 | local get_hex = require('cokeline.hlgroups').get_hl_attr
158 |
159 | require('cokeline').setup({
160 | default_hl = {
161 | fg = function(buffer)
162 | return
163 | buffer.is_focused
164 | and get_hex('Normal', 'fg')
165 | or get_hex('Comment', 'fg')
166 | end,
167 | bg = 'NONE',
168 | },
169 | components = {
170 | {
171 | text = function(buffer) return (buffer.index ~= 1) and '▏' or '' end,
172 | fg = function() return get_hex('Normal', 'fg') end
173 | },
174 | {
175 | text = function(buffer) return ' ' .. buffer.devicon.icon end,
176 | fg = function(buffer) return buffer.devicon.color end,
177 | },
178 | {
179 | text = function(buffer) return buffer.filename .. ' ' end,
180 | bold = function(buffer) return buffer.is_focused end
181 | },
182 | {
183 | text = '',
184 | on_click = function(_, _, _, _, buffer)
185 | buffer:delete()
186 | end
187 | },
188 | {
189 | text = ' ',
190 | },
191 | },
192 | })
193 | ```
194 |
195 |
196 |
197 | 
198 |
199 |
200 |
201 | ### Dynamic rendering
202 |
203 |
204 |
205 | Even when you have a lot of buffers open, `nvim-cokeline` is rendered to always
206 | keep the focused buffer visible and in the middle of the bufferline. Also, if a
207 | buffer doesn't fit entirely we still try to include as much of it as possible
208 | before cutting off the rest.
209 |
210 |
211 |
212 | 
213 |
214 |
215 |
216 | ### LSP support
217 |
218 | If a buffer has an LSP client attached to it, you can configure the style of a
219 | component to change based on how many errors, warnings, infos and hints are
220 | reported by the LSP.
221 |
222 |
223 |
224 | 
225 |
226 |
227 |
228 | ### Buffer pick
229 |
230 | You can focus and close any buffer by typing its `pick_letter`. Letters are
231 | assigned by filename by default (e.g. `foo.txt` gets the letter `f`), and by
232 | keyboard reachability if the letter is already assigned to another buffer.
233 |
234 |
235 |
236 |
237 | Click to see configuration
238 |
239 | ```lua
240 | local is_picking_focus = require('cokeline.mappings').is_picking_focus
241 | local is_picking_close = require('cokeline.mappings').is_picking_close
242 | local get_hex = require('cokeline.hlgroups').get_hl_attr
243 |
244 | local red = vim.g.terminal_color_1
245 | local yellow = vim.g.terminal_color_3
246 |
247 | require('cokeline').setup({
248 | default_hl = {
249 | fg = function(buffer)
250 | return
251 | buffer.is_focused
252 | and get_hex('Normal', 'fg')
253 | or get_hex('Comment', 'fg')
254 | end,
255 | bg = function() return get_hex('ColorColumn', 'bg') end,
256 | },
257 |
258 | components = {
259 | {
260 | text = function(buffer) return (buffer.index ~= 1) and '▏' or '' end,
261 | },
262 | {
263 | text = ' ',
264 | },
265 | {
266 | text = function(buffer)
267 | return
268 | (is_picking_focus() or is_picking_close())
269 | and buffer.pick_letter .. ' '
270 | or buffer.devicon.icon
271 | end,
272 | fg = function(buffer)
273 | return
274 | (is_picking_focus() and yellow)
275 | or (is_picking_close() and red)
276 | or buffer.devicon.color
277 | end,
278 | italic = function()
279 | return
280 | (is_picking_focus() or is_picking_close())
281 | end,
282 | bold = function()
283 | return
284 | (is_picking_focus() or is_picking_close())
285 | end
286 | },
287 | {
288 | text = ' ',
289 | },
290 | {
291 | text = function(buffer) return buffer.filename .. ' ' end,
292 | bold = function(buffer) return buffer.is_focused end,
293 | },
294 | {
295 | text = '',
296 | on_click = function(_, _, _, _, buffer)
297 | buffer:delete()
298 | end,
299 | },
300 | {
301 | text = ' ',
302 | },
303 | },
304 | })
305 | ```
306 |
307 |
308 |
309 | 
310 |
311 |
312 |
313 | ### Sidebars
314 |
315 | You can add a left sidebar to integrate nicely with file explorer plugins like
316 | [nvim-tree.lua](https://github.com/nvim-tree/nvim-tree.lua),
317 | [CHADTree](https://github.com/ms-jpq/chadtree) or
318 | [NERDTree](https://github.com/preservim/nerdtree).
319 |
320 |
321 |
322 |
323 | Click to see configuration
324 |
325 | ```lua
326 | local get_hex = require('cokeline.hlgroups').get_hl_attr
327 |
328 | local yellow = vim.g.terminal_color_3
329 |
330 | require('cokeline').setup({
331 | default_hl = {
332 | fg = function(buffer)
333 | return
334 | buffer.is_focused
335 | and get_hex('Normal', 'fg')
336 | or get_hex('Comment', 'fg')
337 | end,
338 | bg = function() return get_hex('ColorColumn', 'bg') end,
339 | },
340 |
341 | sidebar = {
342 | filetype = {'NvimTree', 'neo-tree'},
343 | components = {
344 | {
345 | text = function(buf)
346 | return buf.filetype
347 | end,
348 | fg = yellow,
349 | bg = function() return get_hex('NvimTreeNormal', 'bg') end,
350 | bold = true,
351 | },
352 | }
353 | },
354 |
355 | components = {
356 | {
357 | text = function(buffer) return (buffer.index ~= 1) and '▏' or '' end,
358 | },
359 | {
360 | text = ' ',
361 | },
362 | {
363 | text = function(buffer)
364 | return buffer.devicon.icon
365 | end,
366 | fg = function(buffer)
367 | return buffer.devicon.color
368 | end,
369 | },
370 | {
371 | text = ' ',
372 | },
373 | {
374 | text = function(buffer) return buffer.filename .. ' ' end,
375 | bold = function(buffer)
376 | return buffer.is_focused
377 | end,
378 | },
379 | {
380 | text = '',
381 | on_click = function(_, _, _, _, buffer)
382 | buffer:delete()
383 | end,
384 | },
385 | {
386 | text = ' ',
387 | },
388 | },
389 | })
390 | ```
391 |
392 |
393 |
394 | 
395 |
396 |
397 |
398 | ### Unique buffer names
399 |
400 | When files with the same filename belonging to different directories are opened
401 | simultaneously, you can include a unique filetree prefix to distinguish between
402 | them:
403 |
404 |
405 |
406 | 
407 |
408 |
409 |
410 | ### Clickable buffers
411 |
412 | Left click on a buffer to focus it, and right click to delete it. Alternatively, define custom click handlers for each component that override the default behavior.
413 |
414 |
415 |
416 | 
417 |
418 |
419 |
420 | ### Hover events
421 |
422 | Each component has access to an is_hovered property, and can be given custom `on_mouse_enter` and `on_mouse_leave` handlers, allowing for implementations of close buttons, diagnostic previews, and more complex funcionality.
423 |
424 | Note: requires `:h 'mousemoveevent'`
425 |
426 |
427 |
428 | 
429 |
430 | 
431 |
432 |
433 |
434 | ### Buffer re-ordering (including mouse-drag reordering)
435 |
436 |
437 |
438 | 
439 |
440 |
441 |
442 | ### Close icons
443 |
444 |
445 |
446 | 
447 |
448 |
449 |
450 | ### Buffer history tracking
451 |
452 | ```lua
453 | require("cokeline.history"):last():focus()
454 | ```
455 |
456 | If you are a user of [`resession.nvim`](https://github.com/stevearc/resession.nvim), cokeline's history will be restored along with
457 | the rest of your sessions.
458 |
459 | ## :electric_plug: Requirements
460 |
461 | The two main requirements are Neovim 0.5+ and the `termguicolors` option to be
462 | set. If you want to display devicons in your bufferline you'll also need the
463 | [nvim-tree/nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons)
464 | plugin and a patched font (see [Nerd Fonts](https://www.nerdfonts.com/)).
465 |
466 | As of v0.4.0, [nvim-lua/plenary.nvim](https://github.com/nvim-lua/plenary.nvim) is required as well.
467 |
468 | ## :package: Installation
469 |
470 | ### Lua
471 |
472 | #### With lazy.nvim
473 |
474 | ```lua
475 | require("lazy").setup({
476 | {
477 | "willothy/nvim-cokeline",
478 | dependencies = {
479 | "nvim-lua/plenary.nvim", -- Required for v0.4.0+
480 | "nvim-tree/nvim-web-devicons", -- If you want devicons
481 | "stevearc/resession.nvim" -- Optional, for persistent history
482 | },
483 | config = true
484 | }
485 | })
486 | ```
487 |
488 | ### Vimscript
489 |
490 | If your config is still written in Vimscript and you use
491 | [vim-plug](https://github.com/junegunn/vim-plug):
492 |
493 | ```vim
494 | call plug#begin('~/.config/nvim/plugged')
495 | " ...
496 | Plug 'nvim-lua/plenary.nvim' " Required for v0.4.0+
497 | Plug 'nvim-tree/nvim-web-devicons' " If you want devicons
498 | Plug 'willothy/nvim-cokeline'
499 | " ...
500 | call plug#end()
501 |
502 | set termguicolors
503 | lua << EOF
504 | require('cokeline').setup()
505 | EOF
506 | ```
507 |
508 | ## :wrench: Configuration
509 |
510 | > **note**
511 | > Check out the [wiki](https://github.com/willothy/nvim-cokeline/wiki) for more details and API documentation.
512 |
513 | All the configuration is done by changing the contents of the Lua table passed to
514 | the `setup` function.
515 |
516 | The valid keys are:
517 |
518 | ```lua
519 | require('cokeline').setup({
520 | -- Only show the bufferline when there are at least this many visible buffers.
521 | -- default: `1`.
522 | ---@type integer
523 | show_if_buffers_are_at_least = 1,
524 |
525 | buffers = {
526 | -- A function to filter out unwanted buffers. Takes a buffer table as a
527 | -- parameter (see the following section for more infos) and has to return
528 | -- either `true` or `false`.
529 | -- default: `false`.
530 | ---@type false | fun(buf: Buffer):boolean
531 | filter_valid = false,
532 |
533 | -- A looser version of `filter_valid`, use this function if you still
534 | -- want the `cokeline-{switch,focus}-{prev,next}` mappings to work for
535 | -- these buffers without displaying them in your bufferline.
536 | -- default: `false`.
537 | ---@type false | fun(buf: Buffer):boolean
538 | filter_visible = false,
539 |
540 | -- Which buffer to focus when a buffer is deleted, `prev` focuses the
541 | -- buffer to the left of the deleted one while `next` focuses the one the
542 | -- right.
543 | -- default: 'next'.
544 | focus_on_delete = 'prev' | 'next',
545 |
546 | -- If set to `last` new buffers are added to the end of the bufferline,
547 | -- if `next` they are added next to the current buffer.
548 | -- if set to `directory` buffers are sorted by their full path.
549 | -- if set to `number` buffers are sorted by bufnr, as in default Neovim
550 | -- default: 'last'.
551 | ---@type 'last' | 'next' | 'directory' | 'number' | fun(a: Buffer, b: Buffer):boolean
552 | new_buffers_position = 'last',
553 |
554 | -- If true, right clicking a buffer will close it
555 | -- The close button will still work normally
556 | -- Default: true
557 | ---@type boolean
558 | delete_on_right_click = true,
559 | },
560 |
561 | mappings = {
562 | -- Controls what happens when the first (last) buffer is focused and you
563 | -- try to focus/switch the previous (next) buffer. If `true` the last
564 | -- (first) buffers gets focused/switched, if `false` nothing happens.
565 | -- default: `true`.
566 | ---@type boolean
567 | cycle_prev_next = true,
568 |
569 | -- Disables mouse mappings
570 | -- default: `false`.
571 | ---@type boolean
572 | disable_mouse = false,
573 | },
574 |
575 | -- Maintains a history of focused buffers using a ringbuffer
576 | history = {
577 | ---@type boolean
578 | enabled = true,
579 | ---The number of buffers to save in the history
580 | ---@type integer
581 | size = 2
582 | },
583 |
584 | rendering = {
585 | -- The maximum number of characters a rendered buffer is allowed to take
586 | -- up. The buffer will be truncated if its width is bigger than this
587 | -- value.
588 | -- default: `999`.
589 | ---@type integer
590 | max_buffer_width = 999,
591 | },
592 |
593 | pick = {
594 | -- Whether to use the filename's first letter first before
595 | -- picking a letter from the valid letters list in order.
596 | -- default: `true`
597 | ---@type boolean
598 | use_filename = true,
599 |
600 | -- The list of letters that are valid as pick letters. Sorted by
601 | -- keyboard reachability by default, but may require tweaking for
602 | -- non-QWERTY keyboard layouts.
603 | -- default: `'asdfjkl;ghnmxcvbziowerutyqpASDFJKLGHNMXCVBZIOWERTYQP'`
604 | ---@type string
605 | letters = 'asdfjkl;ghnmxcvbziowerutyqpASDFJKLGHNMXCVBZIOWERTYQP',
606 | },
607 |
608 | -- The default highlight group values.
609 | -- The `fg`, `bg`, and `sp` keys are either colors in hexadecimal format or
610 | -- functions taking a `buffer` parameter and returning a color in
611 | -- hexadecimal format. Style attributes work the same way, but functions
612 | -- should return boolean values.
613 | default_hl = {
614 | -- default: `ColorColumn`'s background color for focused buffers,
615 | -- `Normal`'s foreground color for unfocused ones.
616 | ---@type nil | string | fun(buffer: Buffer): string
617 | fg = function(buffer)
618 | local hlgroups = require("cokeline.hlgroups")
619 | return buffer.is_focused and hlgroups.get_hl_attr("ColorColumn", "bg")
620 | or hlgroups.get_hl_attr("Normal", "fg")
621 | end,
622 |
623 | -- default: `Normal`'s foreground color for focused buffers,
624 | -- `ColorColumn`'s background color for unfocused ones.
625 | -- default: `Normal`'s foreground color.
626 | ---@type nil | string | function(buffer: Buffer): string,
627 | bg = function(buffer)
628 | local hlgroups = require("cokeline.hlgroups")
629 | return buffer.is_focused and hlgroups.get_hl_attr("Normal", "fg")
630 | or hlgroups.get_hl_attr("ColorColumn", "bg")
631 | end,
632 |
633 | -- default: unset.
634 | ---@type nil | string | function(buffer): string,
635 | sp = nil,
636 |
637 | ---@type nil | boolean | fun(buf: Buffer):boolean
638 | bold = nil,
639 | ---@type nil | boolean | fun(buf: Buffer):boolean
640 | italic = nil,
641 | ---@type nil | boolean | fun(buf: Buffer):boolean
642 | underline = nil,
643 | ---@type nil | boolean | fun(buf: Buffer):boolean
644 | undercurl = nil,
645 | ---@type nil | boolean | fun(buf: Buffer):boolean
646 | strikethrough = nil,
647 | },
648 |
649 | -- The highlight group used to fill the tabline space
650 | fill_hl = 'TabLineFill',
651 |
652 | -- A list of components to be rendered for each buffer. Check out the section
653 | -- below explaining what this value can be set to.
654 | -- default: see `/lua/cokeline/config.lua`
655 | ---@type Component[]
656 | components = {},
657 |
658 | -- Custom areas can be displayed on the right hand side of the bufferline.
659 | -- They act identically to buffer components, except their methods don't take a Buffer object.
660 | -- If you want a rhs component to be stateful, you can wrap it in a closure containing state.
661 | ---@type Component[] | false
662 | rhs = {},
663 |
664 | -- Tabpages can be displayed on either the left or right of the bufferline.
665 | -- They act the same as other components, except they are passed TabPage objects instead of
666 | -- buffer objects.
667 | ---@type table | false
668 | tabs = {
669 | placement = "left" | "right",
670 | ---@type Component[]
671 | components = {}
672 | },
673 |
674 | -- Left sidebar to integrate nicely with file explorer plugins.
675 | -- This is a table containing a `filetype` key and a list of `components` to
676 | -- be rendered in the sidebar.
677 | -- The last component will be automatically space padded if necessary
678 | -- to ensure the sidebar and the window below it have the same width.
679 | ---@type table | false
680 | sidebar = {
681 | ---@type string | string[]
682 | filetype = { "NvimTree", "neo-tree", "SidebarNvim" },
683 | ---@type Component[]
684 | components = {},
685 | },
686 | })
687 | ```
688 |
689 | ### So what's `function(buffer)`?
690 |
691 | Some of the configuration options can be functions that take a [`Buffer`](https://github.com/willothy/nvim-cokeline/wiki/Buffer) as a
692 | single parameter. This is useful as it allows users to set the values of
693 | components dynamically based on the buffer that component is being rendered
694 | for.
695 |
696 | The `Buffer` type is just a Lua table with the following keys:
697 |
698 | ```lua
699 | Buffer = {
700 | -- The buffer's order in the bufferline (1 for the first buffer, 2 for the
701 | -- second one, etc.).
702 | index = int,
703 |
704 | -- The buffer's internal number as reported by `:ls`.
705 | number = int,
706 |
707 | ---@type boolean
708 | is_focused = false,
709 |
710 | ---@type boolean
711 | is_modified = false,
712 |
713 | ---@type boolean
714 | is_readonly = false,
715 |
716 | -- The buffer is the first visible buffer in the tab bar
717 | ---@type boolean
718 | is_first = false,
719 |
720 | -- The buffer is the last visible buffer in the tab bar
721 | ---@type boolean
722 | is_last = false,
723 |
724 | -- The mouse is hovering over the current component in the buffer
725 | -- This is a special variable in that it will only be true for the hovered *component*
726 | -- on render. This is to allow components to respond to hover events individually without managing
727 | -- component state.
728 | ---@type boolean
729 | is_hovered = false,
730 |
731 | -- The mouse is hovering over the buffer (true for all components)
732 | ---@type boolean
733 | buf_hovered = false,
734 |
735 | -- The buffer's type as reported by `:echo &buftype`.
736 | ---@type string
737 | ---@type string
738 | type = '',
739 |
740 | -- The buffer's filetype as reported by `:echo &filetype`.
741 | ---@type string
742 | filetype = '',
743 |
744 | -- The buffer's full path.
745 | ---@type string
746 | path = '',
747 |
748 | -- The buffer's filename.
749 | ---@type string
750 | filename = 'string',
751 |
752 | -- A unique prefix used to distinguish buffers with the same filename
753 | -- stored in different directories. For example, if we have two files
754 | -- `bar/foo.md` and `baz/foo.md`, then the first will have `bar/` as its
755 | -- unique prefix and the second one will have `baz/`.
756 | ---@type string
757 | unique_prefix = '',
758 |
759 | -- The letter that is displayed when picking a buffer to either focus or
760 | -- close it.
761 | ---@type string
762 | pick_letter = 'char',
763 |
764 | -- This needs the `nvim-tree/nvim-web-devicons` plugin to be installed.
765 | devicon = {
766 | -- An icon representing the buffer's filetype.
767 | ---@type string
768 | icon = 'string',
769 |
770 | -- The colors of the devicon in hexadecimal format (useful to be passed
771 | -- to a component's `fg` field (see the `Components` section).
772 | color = '#rrggbb',
773 | },
774 |
775 | -- The values in this table are the ones reported by Neovim's built in
776 | -- LSP interface.
777 | diagnostics = {
778 | ---@type integer
779 | errors = 0,
780 | ---@type integer
781 | warnings = 0,
782 | ---@type integer
783 | infos = 0,
784 | ---@type integer
785 | hints = 0,
786 | },
787 | }
788 | ```
789 |
790 | It also has methods that can be used in component event handlers:
791 |
792 | ```lua
793 | ---@param self Buffer
794 | ---Deletes the buffer
795 | function Buffer:delete() end
796 |
797 | ---@param self Buffer
798 | ---Focuses the buffer
799 | function Buffer:focus() end
800 |
801 | ---@param self Buffer
802 | ---@return number
803 | ---Returns the number of lines in the buffer
804 | function Buffer:lines() end
805 |
806 | ---@param self Buffer
807 | ---@return string[]
808 | ---Returns the buffer's lines
809 | function Buffer:text() end
810 |
811 | ---@param buf Buffer
812 | ---@return boolean
813 | ---Returns true if the buffer is valid
814 | function Buffer:is_valid() end
815 | ```
816 |
817 | ### What about [`TabPage`](https://github.com/willothy/nvim-cokeline/wiki/TabPage)s?
818 |
819 | Each method on a tab component is passed a `TabPage` object as an argument.
820 |
821 | `TabPage`, like `Buffer`, is simply a Lua table with some relevant data attached.
822 |
823 | ```lua
824 | TabPage = {
825 | -- The tabpage number, as reported by `nvim_list_tabpages`
826 | ---@type integer
827 | number = 0,
828 | -- A list of Window objects contained in the TabPage (see wiki for more info)
829 | ---@type Window[]
830 | windows = {},
831 | -- The currently focused window in the TabPage
832 | ---@type Window
833 | focused = nil,
834 | -- True if the TabPage is the current TabPage
835 | ---@type boolean
836 | is_active = true,
837 | -- True if the TabPage is first in the list
838 | ---@type boolean
839 | is_first = false,
840 | -- True if the TabPage is last in the list
841 | ---@type boolean
842 | is_last = false
843 | }
844 | ```
845 |
846 | ### And [`components`](https://github.com/willothy/nvim-cokeline/wiki/Component)?
847 |
848 | You can configure what each buffer in your bufferline will be composed of by
849 | passing a list of components to the `setup` function.
850 |
851 | For example, let's imagine we want to construct a very minimal bufferline
852 | where the only things we're displaying for each buffer are its index, its
853 | filename and a close button.
854 |
855 | Then in our `setup` function we'd have:
856 |
857 | ```lua
858 | require('cokeline').setup({
859 | -- ...
860 | components = {
861 | {
862 | text = function(buffer) return ' ' .. buffer.index end,
863 | },
864 | {
865 | text = function(buffer) return ' ' .. buffer.filename .. ' ' end,
866 | },
867 | {
868 | text = '',
869 | on_click = function(_, _, _, _, buffer)
870 | buffer:delete()
871 | end
872 | },
873 | {
874 | text = ' ',
875 | }
876 | }
877 | })
878 | ```
879 |
880 | in this case every buffer would be composed of four components: the first
881 | displaying a space followed by the buffer index, the second one the filename
882 | padded by a space on each side, then a close button that allows us to
883 | `:bdelete` the buffer by left-clicking on it, and finally an extra space.
884 |
885 | This way of dividing each buffer into distinct components, combined with the
886 | ability to define every component's text and color depending on some property
887 | of the buffer we're rendering, allows for great customizability.
888 |
889 | Every component passed to the `components` list has to be a table of the form:
890 |
891 | ```lua
892 | {
893 |
894 | ---@type string | fun(buffer: Buffer): string
895 | text = "",
896 |
897 | -- The foreground, backgrond and style of the component
898 | ---@type nil | string | fun(buffer: Buffer): string
899 | fg = '#rrggbb',
900 | ---@type nil | string | fun(buffer: Buffer): string
901 | bg = '#rrggbb',
902 | ---@type nil | string | fun(buffer: Buffer): string
903 | sp = '#rrggbb',
904 | ---@type nil | boolean | fun(buffer: Buffer): boolean
905 | bold = false,
906 | ---@type nil | boolean | fun(buffer: Buffer): boolean
907 | italic = false,
908 | ---@type nil | boolean | fun(buffer: Buffer): boolean
909 | underline = false,
910 | ---@type nil | boolean | fun(buffer: Buffer): boolean
911 | undercurl = false,
912 | ---@type nil | boolean | fun(buffer: Buffer): boolean
913 | strikethrough = false,
914 |
915 | -- Or, alternatively, the name of the highlight group
916 | ---@type nil | string | fun(buffer: Buffer): string
917 | highlight = nil,
918 |
919 | -- If `true` the buffer will be deleted when this component is
920 | -- left-clicked (usually used to implement close buttons, overrides `on_click`).
921 | -- deprecated, it is recommended to use the Buffer:delete() method in an on_click event
922 | -- to implement close buttons instead.
923 | ---@type boolean
924 | delete_buffer_on_left_click = false,
925 |
926 | -- Handles click event for a component
927 | -- If not set, component will have the default click behavior
928 | -- buffer is a Buffer object, not a bufnr
929 | ---@type nil | fun(idx: integer, clicks: integer, button: string, mods: string, buffer: Buffer)
930 | on_click = nil,
931 |
932 | -- Called on a component when hovered
933 | ---@type nil | function(buffer: Buffer, mouse_col: integer)
934 | on_mouse_enter = nil,
935 |
936 | -- Called on a component when unhovered
937 | ---@type nil | function(buffer: Buffer, mouse_col: integer)
938 | on_mouse_leave = nil,
939 |
940 | truncation = {
941 | -- default: index of the component in the `components` table (1 for the
942 | -- first component, 2 for the second, etc.).
943 | ---@type integer
944 | priority = 1,
945 |
946 | -- default: `right`.
947 | ---@type 'left' | 'middle' | 'right'
948 | direction = 'left' | 'middle' | 'right',
949 | },
950 | }
951 | ```
952 |
953 | the `text` key is the only one that has to be set, all the others are optional
954 | and can be omitted.
955 |
956 | The `truncation` table controls what happens when a buffer is too long to be
957 | displayed in its entirety.
958 |
959 | More specifically, if a buffer's width (given by the sum of the widths of all
960 | its components) is bigger than the `rendering.max_buffer_width` config option,
961 | the buffer will be truncated.
962 |
963 | The default behaviour is truncate the buffer by dropping components from right
964 | to left, with the text of the last component that's included also being
965 | shortened from right to left. This can be modified by changing the values of
966 | the `truncation.priority` and `truncation.direction` keys.
967 |
968 | The `truncation.priority` controls the order in which components are dropped:
969 | the first component to be dropped will be the one with the lowest priority. If
970 | that's still not enough to bring the width of the buffer within the
971 | `rendering.max_buffer_width` limit, the component with the second lowest
972 | priority will be dropped, and so on. Note that a higher priority means a
973 | smaller integer value: a component with a priority of 5 will be dropped
974 | _after_ a component with a priority of 6, even though 6 > 5.
975 |
976 | The `truncation.direction` key simply controls from which direction a component
977 | is shortened. For example, you might want to set the `truncation.direction` of
978 | a component displaying a filename to `'middle'` or `'left'`, so that if
979 | the filename has to be shortened you'll still be able to see its extension,
980 | like in the following example (where it's set to `'left'`):
981 |
982 |
983 |
984 | 
985 |
986 |
987 |
988 | #### What about [`history`](https://github.com/willothy/nvim-cokeline/wiki/History)?
989 |
990 | The History keeps track of the buffers you access using a ringbuffer, and provides
991 | an API for accessing Buffer objects from the history.
992 |
993 | You can access the history using `require("cokeline.history")`, or through the global `_G.cokeline.history`.
994 |
995 | The `History` object provides these methods:
996 |
997 | ```lua
998 | History = {}
999 |
1000 | ---Adds a Buffer object to the history
1001 | ---@type bufnr integer
1002 | function History:push(bufnr)
1003 | end
1004 |
1005 | ---Removes and returns the oldest Buffer object in the history
1006 | ---@return Buffer?
1007 | function History:pop()
1008 | end
1009 |
1010 | ---Returns a list of Buffer objects in the history,
1011 | ---ordered from oldest to newest
1012 | ---@return Buffer[]
1013 | function History:list()
1014 | end
1015 |
1016 | ---Returns an iterator of Buffer objects in the history,
1017 | ---ordered from oldest to newest
1018 | ---@return fun(): Buffer?
1019 | function History:iter()
1020 | end
1021 |
1022 | ---Get a Buffer object by history index
1023 | ---@param idx integer
1024 | ---@return Buffer?
1025 | function History:get(idx)
1026 | end
1027 |
1028 | ---Get a Buffer object representing the last-accessed buffer (before the current one)
1029 | ---@return Buffer?
1030 | function History:last()
1031 | end
1032 |
1033 | ---Returns true if the history is empty
1034 | ---@return boolean
1035 | function History:is_empty()
1036 | end
1037 |
1038 | ---Returns the maximum number of buffers that can be stored in the history
1039 | ---@return integer
1040 | function History:capacity()
1041 | end
1042 |
1043 | ---Returns true if the history contains the given buffer
1044 | ---@param bufnr integer
1045 | ---@return boolean
1046 | function History:contains(bufnr)
1047 | end
1048 |
1049 | ---Returns the number of buffers in the history
1050 | ---@return integer
1051 | function History:len()
1052 | end
1053 | ```
1054 |
1055 | ## :musical_keyboard: Mappings
1056 |
1057 | You can use the `mappings` module to create mappings from Lua:
1058 |
1059 | ```lua
1060 | vim.keymap.set("n", "bp", function()
1061 | require('cokeline.mappings').pick("focus")
1062 | end, { desc = "Pick a buffer to focus" })
1063 | ```
1064 |
1065 | Alternatively, we expose the following `` mappings which can be used as the right hand
1066 | side of other mappings:
1067 |
1068 | ```
1069 | -- Focus the previous/next buffer
1070 | (cokeline-focus-prev)
1071 | (cokeline-focus-next)
1072 |
1073 | -- Switch the position of the current buffer with the previous/next buffer.
1074 | (cokeline-switch-prev)
1075 | (cokeline-switch-next)
1076 |
1077 | -- Focuses the buffer with index `i`.
1078 | (cokeline-focus-i)
1079 |
1080 | -- Switches the position of the current buffer with the buffer of index `i`.
1081 | (cokeline-switch-i)
1082 |
1083 | -- Focus a buffer by its `pick_letter`.
1084 | (cokeline-pick-focus)
1085 |
1086 | -- Close a buffer by its `pick_letter`.
1087 | (cokeline-pick-close)
1088 | ```
1089 |
1090 | A possible configuration could be:
1091 |
1092 | ```lua
1093 | local map = vim.api.nvim_set_keymap
1094 |
1095 | map("n", "", "(cokeline-focus-prev)", { silent = true })
1096 | map("n", "", "(cokeline-focus-next)", { silent = true })
1097 | map("n", "p", "(cokeline-switch-prev)", { silent = true })
1098 | map("n", "n", "(cokeline-switch-next)", { silent = true })
1099 |
1100 | for i = 1, 9 do
1101 | map(
1102 | "n",
1103 | (""):format(i),
1104 | ("(cokeline-focus-%s)"):format(i),
1105 | { silent = true }
1106 | )
1107 | map(
1108 | "n",
1109 | ("%s"):format(i),
1110 | ("(cokeline-switch-%s)"):format(i),
1111 | { silent = true }
1112 | )
1113 | end
1114 |
1115 | ```
1116 |
--------------------------------------------------------------------------------
/doc/cokeline.txt:
--------------------------------------------------------------------------------
1 | *cokeline.txt* For NVIM >=0.7 Last change: 2025 January 24
2 |
3 | ==============================================================================
4 | Table of Contents *cokeline-table-of-contents*
5 |
6 | - Features |cokeline-features|
7 | - Requirements |cokeline-requirements|
8 | - Installation |cokeline-installation|
9 | - Configuration |cokeline-configuration|
10 | - Mappings |cokeline-mappings|
11 |
12 |
13 | nvim-cokeline
14 |
15 | A Neovim bufferline for people with addictive personalities
16 |
17 | The goal of this plugin is not to be an opinionated bufferline with (more or
18 | less) limited customization options. Rather, it tries to provide a general
19 | framework allowing you to build **your** ideal bufferline, whatever that might
20 | look like.
21 |
22 |
23 | FEATURES *cokeline-features*
24 |
25 |
26 | ENDLESSLY CUSTOMIZABLE ~
27 |
28 | `nvim-cokeline` aims to be the most customizable bufferline plugin around. If
29 | you have an idea in mind of what your bufferline should look like, you should
30 | be able to make it look that way. If you can’t, open an issue and we’ll try
31 | to make it happen!
32 |
33 |
34 | DYNAMIC RENDERING ~
35 |
36 | Even when you have a lot of buffers open, `nvim-cokeline` is rendered to always
37 | keep the focused buffer visible and in the middle of the bufferline. Also, if a
38 | buffer doesn’t fit entirely we still try to include as much of it as possible
39 | before cutting off the rest.
40 |
41 |
42 | LSP SUPPORT ~
43 |
44 | If a buffer has an LSP client attached to it, you can configure the style of a
45 | component to change based on how many errors, warnings, infos and hints are
46 | reported by the LSP.
47 |
48 |
49 | BUFFER PICK ~
50 |
51 | You can focus and close any buffer by typing its `pick_letter`. Letters are
52 | assigned by filename by default (e.g. `foo.txt` gets the letter `f`), and by
53 | keyboard reachability if the letter is already assigned to another buffer.
54 |
55 |
56 | SIDEBARS ~
57 |
58 | You can add a left sidebar to integrate nicely with file explorer plugins like
59 | nvim-tree.lua , CHADTree
60 | or NERDTree
61 | .
62 |
63 |
64 | UNIQUE BUFFER NAMES ~
65 |
66 | When files with the same filename belonging to different directories are opened
67 | simultaneously, you can include a unique filetree prefix to distinguish between
68 | them:
69 |
70 |
71 | CLICKABLE BUFFERS ~
72 |
73 | Left click on a buffer to focus it, and right click to delete it.
74 | Alternatively, define custom click handlers for each component that override
75 | the default behavior.
76 |
77 |
78 | HOVER EVENTS ~
79 |
80 | Each component has access to an is_hovered property, and can be given custom
81 | `on_mouse_enter` and `on_mouse_leave` handlers, allowing for implementations of
82 | close buttons, diagnostic previews, and more complex funcionality.
83 |
84 | Note: requires |'mousemoveevent'|
85 |
86 |
87 | BUFFER RE-ORDERING (INCLUDING MOUSE-DRAG REORDERING) ~
88 |
89 |
90 | CLOSE ICONS ~
91 |
92 |
93 | BUFFER HISTORY TRACKING ~
94 |
95 | >lua
96 | require("cokeline.history"):last():focus()
97 | <
98 |
99 | If you are a user of `resession.nvim`
100 | , cokeline’s history will be
101 | restored along with the rest of your sessions.
102 |
103 |
104 | REQUIREMENTS *cokeline-requirements*
105 |
106 | The two main requirements are Neovim 0.5+ and the `termguicolors` option to be
107 | set. If you want to display devicons in your bufferline you’ll also need the
108 | nvim-tree/nvim-web-devicons
109 | plugin and a patched font (see Nerd Fonts ).
110 |
111 | As of v0.4.0, nvim-lua/plenary.nvim
112 | is required as well.
113 |
114 |
115 | INSTALLATION *cokeline-installation*
116 |
117 |
118 | LUA ~
119 |
120 |
121 | WITH LAZY.NVIM
122 |
123 | >lua
124 | require("lazy").setup({
125 | {
126 | "willothy/nvim-cokeline",
127 | dependencies = {
128 | "nvim-lua/plenary.nvim", -- Required for v0.4.0+
129 | "nvim-tree/nvim-web-devicons", -- If you want devicons
130 | "stevearc/resession.nvim" -- Optional, for persistent history
131 | },
132 | config = true
133 | }
134 | })
135 | <
136 |
137 |
138 | VIMSCRIPT ~
139 |
140 | If your config is still written in Vimscript and you use vim-plug
141 |
142 |
143 | >vim
144 | call plug#begin('~/.config/nvim/plugged')
145 | " ...
146 | Plug 'nvim-lua/plenary.nvim' " Required for v0.4.0+
147 | Plug 'nvim-tree/nvim-web-devicons' " If you want devicons
148 | Plug 'willothy/nvim-cokeline'
149 | " ...
150 | call plug#end()
151 |
152 | set termguicolors
153 | lua << EOF
154 | require('cokeline').setup()
155 | EOF
156 | <
157 |
158 |
159 | CONFIGURATION *cokeline-configuration*
160 |
161 |
162 | **note** Check out the wiki
163 | for more details and API documentation.
164 | All the configuration is done by changing the contents of the Lua table passed
165 | to the `setup` function.
166 |
167 | The valid keys are:
168 |
169 | >lua
170 | require('cokeline').setup({
171 | -- Only show the bufferline when there are at least this many visible buffers.
172 | -- default: `1`.
173 | ---@type integer
174 | show_if_buffers_are_at_least = 1,
175 |
176 | buffers = {
177 | -- A function to filter out unwanted buffers. Takes a buffer table as a
178 | -- parameter (see the following section for more infos) and has to return
179 | -- either `true` or `false`.
180 | -- default: `false`.
181 | ---@type false | fun(buf: Buffer):boolean
182 | filter_valid = false,
183 |
184 | -- A looser version of `filter_valid`, use this function if you still
185 | -- want the `cokeline-{switch,focus}-{prev,next}` mappings to work for
186 | -- these buffers without displaying them in your bufferline.
187 | -- default: `false`.
188 | ---@type false | fun(buf: Buffer):boolean
189 | filter_visible = false,
190 |
191 | -- Which buffer to focus when a buffer is deleted, `prev` focuses the
192 | -- buffer to the left of the deleted one while `next` focuses the one the
193 | -- right.
194 | -- default: 'next'.
195 | focus_on_delete = 'prev' | 'next',
196 |
197 | -- If set to `last` new buffers are added to the end of the bufferline,
198 | -- if `next` they are added next to the current buffer.
199 | -- if set to `directory` buffers are sorted by their full path.
200 | -- if set to `number` buffers are sorted by bufnr, as in default Neovim
201 | -- default: 'last'.
202 | ---@type 'last' | 'next' | 'directory' | 'number' | fun(a: Buffer, b: Buffer):boolean
203 | new_buffers_position = 'last',
204 |
205 | -- If true, right clicking a buffer will close it
206 | -- The close button will still work normally
207 | -- Default: true
208 | ---@type boolean
209 | delete_on_right_click = true,
210 | },
211 |
212 | mappings = {
213 | -- Controls what happens when the first (last) buffer is focused and you
214 | -- try to focus/switch the previous (next) buffer. If `true` the last
215 | -- (first) buffers gets focused/switched, if `false` nothing happens.
216 | -- default: `true`.
217 | ---@type boolean
218 | cycle_prev_next = true,
219 |
220 | -- Disables mouse mappings
221 | -- default: `false`.
222 | ---@type boolean
223 | disable_mouse = false,
224 | },
225 |
226 | -- Maintains a history of focused buffers using a ringbuffer
227 | history = {
228 | ---@type boolean
229 | enabled = true,
230 | ---The number of buffers to save in the history
231 | ---@type integer
232 | size = 2
233 | },
234 |
235 | rendering = {
236 | -- The maximum number of characters a rendered buffer is allowed to take
237 | -- up. The buffer will be truncated if its width is bigger than this
238 | -- value.
239 | -- default: `999`.
240 | ---@type integer
241 | max_buffer_width = 999,
242 | },
243 |
244 | pick = {
245 | -- Whether to use the filename's first letter first before
246 | -- picking a letter from the valid letters list in order.
247 | -- default: `true`
248 | ---@type boolean
249 | use_filename = true,
250 |
251 | -- The list of letters that are valid as pick letters. Sorted by
252 | -- keyboard reachability by default, but may require tweaking for
253 | -- non-QWERTY keyboard layouts.
254 | -- default: `'asdfjkl;ghnmxcvbziowerutyqpASDFJKLGHNMXCVBZIOWERTYQP'`
255 | ---@type string
256 | letters = 'asdfjkl;ghnmxcvbziowerutyqpASDFJKLGHNMXCVBZIOWERTYQP',
257 | },
258 |
259 | -- The default highlight group values.
260 | -- The `fg`, `bg`, and `sp` keys are either colors in hexadecimal format or
261 | -- functions taking a `buffer` parameter and returning a color in
262 | -- hexadecimal format. Style attributes work the same way, but functions
263 | -- should return boolean values.
264 | default_hl = {
265 | -- default: `ColorColumn`'s background color for focused buffers,
266 | -- `Normal`'s foreground color for unfocused ones.
267 | ---@type nil | string | fun(buffer: Buffer): string
268 | fg = function(buffer)
269 | local hlgroups = require("cokeline.hlgroups")
270 | return buffer.is_focused and hlgroups.get_hl_attr("ColorColumn", "bg")
271 | or hlgroups.get_hl_attr("Normal", "fg")
272 | end,
273 |
274 | -- default: `Normal`'s foreground color for focused buffers,
275 | -- `ColorColumn`'s background color for unfocused ones.
276 | -- default: `Normal`'s foreground color.
277 | ---@type nil | string | function(buffer: Buffer): string,
278 | bg = function(buffer)
279 | local hlgroups = require("cokeline.hlgroups")
280 | return buffer.is_focused and hlgroups.get_hl_attr("Normal", "fg")
281 | or hlgroups.get_hl_attr("ColorColumn", "bg")
282 | end,
283 |
284 | -- default: unset.
285 | ---@type nil | string | function(buffer): string,
286 | sp = nil,
287 |
288 | ---@type nil | boolean | fun(buf: Buffer):boolean
289 | bold = nil,
290 | ---@type nil | boolean | fun(buf: Buffer):boolean
291 | italic = nil,
292 | ---@type nil | boolean | fun(buf: Buffer):boolean
293 | underline = nil,
294 | ---@type nil | boolean | fun(buf: Buffer):boolean
295 | undercurl = nil,
296 | ---@type nil | boolean | fun(buf: Buffer):boolean
297 | strikethrough = nil,
298 | },
299 |
300 | -- The highlight group used to fill the tabline space
301 | fill_hl = 'TabLineFill',
302 |
303 | -- A list of components to be rendered for each buffer. Check out the section
304 | -- below explaining what this value can be set to.
305 | -- default: see `/lua/cokeline/config.lua`
306 | ---@type Component[]
307 | components = {},
308 |
309 | -- Custom areas can be displayed on the right hand side of the bufferline.
310 | -- They act identically to buffer components, except their methods don't take a Buffer object.
311 | -- If you want a rhs component to be stateful, you can wrap it in a closure containing state.
312 | ---@type Component[] | false
313 | rhs = {},
314 |
315 | -- Tabpages can be displayed on either the left or right of the bufferline.
316 | -- They act the same as other components, except they are passed TabPage objects instead of
317 | -- buffer objects.
318 | ---@type table | false
319 | tabs = {
320 | placement = "left" | "right",
321 | ---@type Component[]
322 | components = {}
323 | },
324 |
325 | -- Left sidebar to integrate nicely with file explorer plugins.
326 | -- This is a table containing a `filetype` key and a list of `components` to
327 | -- be rendered in the sidebar.
328 | -- The last component will be automatically space padded if necessary
329 | -- to ensure the sidebar and the window below it have the same width.
330 | ---@type table | false
331 | sidebar = {
332 | ---@type string | string[]
333 | filetype = { "NvimTree", "neo-tree", "SidebarNvim" },
334 | ---@type Component[]
335 | components = {},
336 | },
337 | })
338 | <
339 |
340 |
341 | SO WHAT’S FUNCTION(BUFFER)? ~
342 |
343 | Some of the configuration options can be functions that take a `Buffer`
344 | as a single parameter.
345 | This is useful as it allows users to set the values of components dynamically
346 | based on the buffer that component is being rendered for.
347 |
348 | The `Buffer` type is just a Lua table with the following keys:
349 |
350 | >lua
351 | Buffer = {
352 | -- The buffer's order in the bufferline (1 for the first buffer, 2 for the
353 | -- second one, etc.).
354 | index = int,
355 |
356 | -- The buffer's internal number as reported by `:ls`.
357 | number = int,
358 |
359 | ---@type boolean
360 | is_focused = false,
361 |
362 | ---@type boolean
363 | is_modified = false,
364 |
365 | ---@type boolean
366 | is_readonly = false,
367 |
368 | -- The buffer is the first visible buffer in the tab bar
369 | ---@type boolean
370 | is_first = false,
371 |
372 | -- The buffer is the last visible buffer in the tab bar
373 | ---@type boolean
374 | is_last = false,
375 |
376 | -- The mouse is hovering over the current component in the buffer
377 | -- This is a special variable in that it will only be true for the hovered *component*
378 | -- on render. This is to allow components to respond to hover events individually without managing
379 | -- component state.
380 | ---@type boolean
381 | is_hovered = false,
382 |
383 | -- The mouse is hovering over the buffer (true for all components)
384 | ---@type boolean
385 | buf_hovered = false,
386 |
387 | -- The buffer's type as reported by `:echo &buftype`.
388 | ---@type string
389 | ---@type string
390 | type = '',
391 |
392 | -- The buffer's filetype as reported by `:echo &filetype`.
393 | ---@type string
394 | filetype = '',
395 |
396 | -- The buffer's full path.
397 | ---@type string
398 | path = '',
399 |
400 | -- The buffer's filename.
401 | ---@type string
402 | filename = 'string',
403 |
404 | -- A unique prefix used to distinguish buffers with the same filename
405 | -- stored in different directories. For example, if we have two files
406 | -- `bar/foo.md` and `baz/foo.md`, then the first will have `bar/` as its
407 | -- unique prefix and the second one will have `baz/`.
408 | ---@type string
409 | unique_prefix = '',
410 |
411 | -- The letter that is displayed when picking a buffer to either focus or
412 | -- close it.
413 | ---@type string
414 | pick_letter = 'char',
415 |
416 | -- This needs the `nvim-tree/nvim-web-devicons` plugin to be installed.
417 | devicon = {
418 | -- An icon representing the buffer's filetype.
419 | ---@type string
420 | icon = 'string',
421 |
422 | -- The colors of the devicon in hexadecimal format (useful to be passed
423 | -- to a component's `fg` field (see the `Components` section).
424 | color = '#rrggbb',
425 | },
426 |
427 | -- The values in this table are the ones reported by Neovim's built in
428 | -- LSP interface.
429 | diagnostics = {
430 | ---@type integer
431 | errors = 0,
432 | ---@type integer
433 | warnings = 0,
434 | ---@type integer
435 | infos = 0,
436 | ---@type integer
437 | hints = 0,
438 | },
439 | }
440 | <
441 |
442 | It also has methods that can be used in component event handlers:
443 |
444 | >lua
445 | ---@param self Buffer
446 | ---Deletes the buffer
447 | function Buffer:delete() end
448 |
449 | ---@param self Buffer
450 | ---Focuses the buffer
451 | function Buffer:focus() end
452 |
453 | ---@param self Buffer
454 | ---@return number
455 | ---Returns the number of lines in the buffer
456 | function Buffer:lines() end
457 |
458 | ---@param self Buffer
459 | ---@return string[]
460 | ---Returns the buffer's lines
461 | function Buffer:text() end
462 |
463 | ---@param buf Buffer
464 | ---@return boolean
465 | ---Returns true if the buffer is valid
466 | function Buffer:is_valid() end
467 | <
468 |
469 |
470 | WHAT ABOUT TABPAGES? ~
471 |
472 | Each method on a tab component is passed a `TabPage` object as an argument.
473 |
474 | `TabPage`, like `Buffer`, is simply a Lua table with some relevant data
475 | attached.
476 |
477 | >lua
478 | TabPage = {
479 | -- The tabpage number, as reported by `nvim_list_tabpages`
480 | ---@type integer
481 | number = 0,
482 | -- A list of Window objects contained in the TabPage (see wiki for more info)
483 | ---@type Window[]
484 | windows = {},
485 | -- The currently focused window in the TabPage
486 | ---@type Window
487 | focused = nil,
488 | -- True if the TabPage is the current TabPage
489 | ---@type boolean
490 | is_active = true,
491 | -- True if the TabPage is first in the list
492 | ---@type boolean
493 | is_first = false,
494 | -- True if the TabPage is last in the list
495 | ---@type boolean
496 | is_last = false
497 | }
498 | <
499 |
500 |
501 | AND COMPONENTS? ~
502 |
503 | You can configure what each buffer in your bufferline will be composed of by
504 | passing a list of components to the `setup` function.
505 |
506 | For example, let’s imagine we want to construct a very minimal bufferline
507 | where the only things we’re displaying for each buffer are its index, its
508 | filename and a close button.
509 |
510 | Then in our `setup` function we’d have:
511 |
512 | >lua
513 | require('cokeline').setup({
514 | -- ...
515 | components = {
516 | {
517 | text = function(buffer) return ' ' .. buffer.index end,
518 | },
519 | {
520 | text = function(buffer) return ' ' .. buffer.filename .. ' ' end,
521 | },
522 | {
523 | text = '',
524 | on_click = function(_, _, _, _, buffer)
525 | buffer:delete()
526 | end
527 | },
528 | {
529 | text = ' ',
530 | }
531 | }
532 | })
533 | <
534 |
535 | in this case every buffer would be composed of four components: the first
536 | displaying a space followed by the buffer index, the second one the filename
537 | padded by a space on each side, then a close button that allows us to
538 | `:bdelete` the buffer by left-clicking on it, and finally an extra space.
539 |
540 | This way of dividing each buffer into distinct components, combined with the
541 | ability to define every component’s text and color depending on some property
542 | of the buffer we’re rendering, allows for great customizability.
543 |
544 | Every component passed to the `components` list has to be a table of the form:
545 |
546 | >lua
547 | {
548 |
549 | ---@type string | fun(buffer: Buffer): string
550 | text = "",
551 |
552 | -- The foreground, backgrond and style of the component
553 | ---@type nil | string | fun(buffer: Buffer): string
554 | fg = '#rrggbb',
555 | ---@type nil | string | fun(buffer: Buffer): string
556 | bg = '#rrggbb',
557 | ---@type nil | string | fun(buffer: Buffer): string
558 | sp = '#rrggbb',
559 | ---@type nil | boolean | fun(buffer: Buffer): boolean
560 | bold = false,
561 | ---@type nil | boolean | fun(buffer: Buffer): boolean
562 | italic = false,
563 | ---@type nil | boolean | fun(buffer: Buffer): boolean
564 | underline = false,
565 | ---@type nil | boolean | fun(buffer: Buffer): boolean
566 | undercurl = false,
567 | ---@type nil | boolean | fun(buffer: Buffer): boolean
568 | strikethrough = false,
569 |
570 | -- Or, alternatively, the name of the highlight group
571 | ---@type nil | string | fun(buffer: Buffer): string
572 | highlight = nil,
573 |
574 | -- If `true` the buffer will be deleted when this component is
575 | -- left-clicked (usually used to implement close buttons, overrides `on_click`).
576 | -- deprecated, it is recommended to use the Buffer:delete() method in an on_click event
577 | -- to implement close buttons instead.
578 | ---@type boolean
579 | delete_buffer_on_left_click = false,
580 |
581 | -- Handles click event for a component
582 | -- If not set, component will have the default click behavior
583 | -- buffer is a Buffer object, not a bufnr
584 | ---@type nil | fun(idx: integer, clicks: integer, button: string, mods: string, buffer: Buffer)
585 | on_click = nil,
586 |
587 | -- Called on a component when hovered
588 | ---@type nil | function(buffer: Buffer, mouse_col: integer)
589 | on_mouse_enter = nil,
590 |
591 | -- Called on a component when unhovered
592 | ---@type nil | function(buffer: Buffer, mouse_col: integer)
593 | on_mouse_leave = nil,
594 |
595 | truncation = {
596 | -- default: index of the component in the `components` table (1 for the
597 | -- first component, 2 for the second, etc.).
598 | ---@type integer
599 | priority = 1,
600 |
601 | -- default: `right`.
602 | ---@type 'left' | 'middle' | 'right'
603 | direction = 'left' | 'middle' | 'right',
604 | },
605 | }
606 | <
607 |
608 | the `text` key is the only one that has to be set, all the others are optional
609 | and can be omitted.
610 |
611 | The `truncation` table controls what happens when a buffer is too long to be
612 | displayed in its entirety.
613 |
614 | More specifically, if a buffer’s width (given by the sum of the widths of all
615 | its components) is bigger than the `rendering.max_buffer_width` config option,
616 | the buffer will be truncated.
617 |
618 | The default behaviour is truncate the buffer by dropping components from right
619 | to left, with the text of the last component that’s included also being
620 | shortened from right to left. This can be modified by changing the values of
621 | the `truncation.priority` and `truncation.direction` keys.
622 |
623 | The `truncation.priority` controls the order in which components are dropped:
624 | the first component to be dropped will be the one with the lowest priority. If
625 | that’s still not enough to bring the width of the buffer within the
626 | `rendering.max_buffer_width` limit, the component with the second lowest
627 | priority will be dropped, and so on. Note that a higher priority means a
628 | smaller integer value: a component with a priority of 5 will be dropped _after_
629 | a component with a priority of 6, even though 6 > 5.
630 |
631 | The `truncation.direction` key simply controls from which direction a component
632 | is shortened. For example, you might want to set the `truncation.direction` of
633 | a component displaying a filename to `'middle'` or `'left'`, so that if the
634 | filename has to be shortened you’ll still be able to see its extension, like
635 | in the following example (where it’s set to `'left'`):
636 |
637 |
638 | WHAT ABOUT HISTORY?
639 |
640 | The History keeps track of the buffers you access using a ringbuffer, and
641 | provides an API for accessing Buffer objects from the history.
642 |
643 | You can access the history using `require("cokeline.history")`, or through the
644 | global `_G.cokeline.history`.
645 |
646 | The `History` object provides these methods:
647 |
648 | >lua
649 | History = {}
650 |
651 | ---Adds a Buffer object to the history
652 | ---@type bufnr integer
653 | function History:push(bufnr)
654 | end
655 |
656 | ---Removes and returns the oldest Buffer object in the history
657 | ---@return Buffer?
658 | function History:pop()
659 | end
660 |
661 | ---Returns a list of Buffer objects in the history,
662 | ---ordered from oldest to newest
663 | ---@return Buffer[]
664 | function History:list()
665 | end
666 |
667 | ---Returns an iterator of Buffer objects in the history,
668 | ---ordered from oldest to newest
669 | ---@return fun(): Buffer?
670 | function History:iter()
671 | end
672 |
673 | ---Get a Buffer object by history index
674 | ---@param idx integer
675 | ---@return Buffer?
676 | function History:get(idx)
677 | end
678 |
679 | ---Get a Buffer object representing the last-accessed buffer (before the current one)
680 | ---@return Buffer?
681 | function History:last()
682 | end
683 |
684 | ---Returns true if the history is empty
685 | ---@return boolean
686 | function History:is_empty()
687 | end
688 |
689 | ---Returns the maximum number of buffers that can be stored in the history
690 | ---@return integer
691 | function History:capacity()
692 | end
693 |
694 | ---Returns true if the history contains the given buffer
695 | ---@param bufnr integer
696 | ---@return boolean
697 | function History:contains(bufnr)
698 | end
699 |
700 | ---Returns the number of buffers in the history
701 | ---@return integer
702 | function History:len()
703 | end
704 | <
705 |
706 |
707 | MAPPINGS *cokeline-mappings*
708 |
709 | You can use the `mappings` module to create mappings from Lua:
710 |
711 | >lua
712 | vim.keymap.set("n", "bp", function()
713 | require('cokeline.mappings').pick("focus")
714 | end, { desc = "Pick a buffer to focus" })
715 | <
716 |
717 | Alternatively, we expose the following `` mappings which can be used as
718 | the right hand side of other mappings:
719 |
720 | >
721 | -- Focus the previous/next buffer
722 | (cokeline-focus-prev)
723 | (cokeline-focus-next)
724 |
725 | -- Switch the position of the current buffer with the previous/next buffer.
726 | (cokeline-switch-prev)
727 | (cokeline-switch-next)
728 |
729 | -- Focuses the buffer with index `i`.
730 | (cokeline-focus-i)
731 |
732 | -- Switches the position of the current buffer with the buffer of index `i`.
733 | (cokeline-switch-i)
734 |
735 | -- Focus a buffer by its `pick_letter`.
736 | (cokeline-pick-focus)
737 |
738 | -- Close a buffer by its `pick_letter`.
739 | (cokeline-pick-close)
740 | <
741 |
742 | A possible configuration could be:
743 |
744 | >lua
745 | local map = vim.api.nvim_set_keymap
746 |
747 | map("n", "", "(cokeline-focus-prev)", { silent = true })
748 | map("n", "", "(cokeline-focus-next)", { silent = true })
749 | map("n", "p", "(cokeline-switch-prev)", { silent = true })
750 | map("n", "n", "(cokeline-switch-next)", { silent = true })
751 |
752 | for i = 1, 9 do
753 | map(
754 | "n",
755 | (""):format(i),
756 | ("(cokeline-focus-%s)"):format(i),
757 | { silent = true }
758 | )
759 | map(
760 | "n",
761 | ("%s"):format(i),
762 | ("(cokeline-switch-%s)"):format(i),
763 | { silent = true }
764 | )
765 | end
766 | <
767 |
768 | Generated by panvimdoc
769 |
770 | vim:tw=78:ts=8:noet:ft=help:norl:
771 |
--------------------------------------------------------------------------------
/lua/cokeline/augroups.lua:
--------------------------------------------------------------------------------
1 | local lazy = require("cokeline.lazy")
2 | local state = lazy("cokeline.state")
3 | local buffers = lazy("cokeline.buffers")
4 | local tabs = lazy("cokeline.tabs")
5 | local config = lazy("cokeline.config")
6 |
7 | local cmd = vim.cmd
8 | local filter = vim.tbl_filter
9 | local fn = vim.fn
10 | local opt = vim.opt
11 |
12 | local toggle = function()
13 | local listed_buffers = fn.getbufinfo({ buflisted = 1 })
14 | opt.showtabline = #listed_buffers > 0 and 2 or 0
15 | end
16 |
17 | local bufnr_to_close
18 |
19 | local close_bufnr = function()
20 | if bufnr_to_close then
21 | cmd("b" .. bufnr_to_close)
22 | bufnr_to_close = nil
23 | end
24 | end
25 |
26 | ---@param bufnr number
27 | local remember_bufnr = function(bufnr)
28 | if #state.valid_buffers == 1 then
29 | return
30 | end
31 |
32 | local deleted_buffer = filter(function(buffer)
33 | return buffer.number == bufnr
34 | end, state.valid_buffers)[1]
35 |
36 | -- Neogit buffers do some weird stuff like closing themselves on buffer
37 | -- change and seem to cause problems. We just ignore them.
38 | -- See https://github.com/noib3/nvim-cokeline/issues/43
39 | if
40 | not deleted_buffer or deleted_buffer.filename:find("Neogit", nil, true)
41 | then
42 | return
43 | end
44 |
45 | local target_index
46 |
47 | if config.buffers.focus_on_delete == "prev" then
48 | target_index = deleted_buffer._valid_index ~= 1
49 | and deleted_buffer._valid_index - 1
50 | or 2
51 | elseif config.buffers.focus_on_delete == "next" then
52 | target_index = deleted_buffer._valid_index ~= #state.valid_buffers
53 | and deleted_buffer._valid_index + 1
54 | or #state.valid_buffers - 1
55 | end
56 |
57 | bufnr_to_close = state.valid_buffers[target_index].number
58 | end
59 |
60 | local setup = function()
61 | local autocmd, augroup =
62 | vim.api.nvim_create_autocmd, vim.api.nvim_create_augroup
63 |
64 | local group = augroup("cokeline_autocmds", { clear = true })
65 |
66 | -- Invalidate the cache on colorscheme change
67 | autocmd("ColorScheme", {
68 | group = group,
69 | callback = function()
70 | require("cokeline.hlgroups")._cache_clear()
71 | end,
72 | })
73 |
74 | autocmd({ "VimEnter", "BufAdd" }, {
75 | group = group,
76 | callback = function()
77 | require("cokeline.augroups").toggle()
78 | end,
79 | })
80 | autocmd({ "BufDelete", "BufWipeout" }, {
81 | group = group,
82 | callback = function(args)
83 | require("cokeline.buffers").release_taken_letter(args.buf)
84 | end,
85 | })
86 | if config.history.enabled then
87 | autocmd("BufLeave", {
88 | group = group,
89 | callback = function(args)
90 | if vim.api.nvim_buf_is_valid(args.buf) then
91 | require("cokeline.history"):push(args.buf)
92 | end
93 | end,
94 | })
95 | end
96 | if config.tabs then
97 | autocmd({ "TabNew", "TabClosed" }, {
98 | group = group,
99 | callback = function()
100 | tabs.fetch_tabs()
101 | end,
102 | })
103 | autocmd("WinEnter", {
104 | group = group,
105 | callback = function()
106 | local win = vim.api.nvim_get_current_win()
107 | local tab = vim.api.nvim_win_get_tabpage(win)
108 | if not state.tab_lookup[tab] then
109 | tabs.fetch_tabs()
110 | return
111 | end
112 | tabs.update_current(tab)
113 | for _, w in ipairs(state.tab_lookup[tab].windows) do
114 | if w.number == win then
115 | state.tab_lookup[tab].focused = w
116 | break
117 | end
118 | end
119 | end,
120 | })
121 | autocmd("BufEnter", {
122 | group = group,
123 | callback = function(args)
124 | local win = vim.api.nvim_get_current_win()
125 | local tab = vim.api.nvim_win_get_tabpage(win)
126 | if not state.tab_lookup[tab] then
127 | tabs.fetch_tabs()
128 | return
129 | end
130 | for _, w in ipairs(state.tab_lookup[tab].windows) do
131 | if w.number == win then
132 | w.buffer = buffers.get_buffer(args.buf)
133 | break
134 | end
135 | end
136 | end,
137 | })
138 | end
139 | end
140 |
141 | return {
142 | close_bufnr = close_bufnr,
143 | remember_bufnr = remember_bufnr,
144 | setup = setup,
145 | toggle = toggle,
146 | }
147 |
--------------------------------------------------------------------------------
/lua/cokeline/buffers.lua:
--------------------------------------------------------------------------------
1 | local lazy = require("cokeline.lazy")
2 | local config = lazy("cokeline.config")
3 | local state = lazy("cokeline.state")
4 |
5 | local has_devicons, rq_devicons = pcall(require, "nvim-web-devicons")
6 |
7 | local concat = table.concat
8 | local sort = table.sort
9 |
10 | local bo = vim.bo
11 | local cmd = vim.cmd
12 | local diagnostic = vim.diagnostic or vim.lsp.diagnostic
13 | local fn = vim.fn
14 | local split = vim.split
15 |
16 | local util = lazy("cokeline.utils")
17 | local iter = require("plenary.iterators").iter
18 |
19 | ---@type bufnr
20 | local current_valid_index
21 |
22 | ---@type string?
23 | local valid_pick_letters
24 |
25 | local taken_pick_letters = {}
26 | local taken_pick_indices = {}
27 | local buf_order = {}
28 |
29 | local M = {}
30 |
31 | ---@alias valid_index number
32 | ---@alias index number
33 | ---@alias bufnr number
34 |
35 | ---@class Buffer
36 | ---@field _valid_index valid_index
37 | ---@field index index
38 | ---@field number bufnr
39 | ---@field type string
40 | ---@field is_focused boolean
41 | ---@field is_modified boolean
42 | ---@field is_readonly boolean
43 | ---@field is_hovered boolean Whether the current component is hovered
44 | ---@field buf_hovered boolean Whether any component in the buffer is hovered
45 | ---@field path string
46 | ---@field unique_prefix string
47 | ---@field filename string
48 | ---@field filetype string
49 | ---@field pick_letter string
50 | ---@field devicon table
51 | ---@field diagnostics table
52 | local Buffer = {}
53 | Buffer.__index = Buffer
54 |
55 | ---@param buffers Iterator|table
56 | ---@return Iterator|table
57 | local compute_unique_prefixes = function(buffers)
58 | local is_windows = fn.has("win32") == 1
59 |
60 | local path_separator = not is_windows and "/" or "\\"
61 |
62 | local prefixes = {}
63 | local paths = {}
64 | buffers = buffers
65 | :map(function(buffer)
66 | prefixes[#prefixes + 1] = {}
67 | paths[#paths + 1] = fn.reverse(
68 | split(
69 | is_windows and buffer.path:gsub("/", "\\") or buffer.path,
70 | path_separator
71 | )
72 | )
73 | return buffer
74 | end)
75 | :tolist()
76 |
77 | for i = 1, #paths do
78 | for j = i + 1, #paths do
79 | local k = 1
80 | while paths[i][k] == paths[j][k] and paths[i][k] do
81 | k = k + 1
82 | prefixes[i][k - 1] = prefixes[i][k - 1] or paths[i][k]
83 | prefixes[j][k - 1] = prefixes[j][k - 1] or paths[j][k]
84 | end
85 | if k ~= 1 then
86 | prefixes[i][k - 1] = prefixes[i][k - 1] or paths[i][k]
87 | prefixes[j][k - 1] = prefixes[j][k - 1] or paths[j][k]
88 | end
89 | end
90 | end
91 |
92 | return iter(buffers):enumerate():map(function(i, buffer)
93 | buffer.unique_prefix = concat({
94 | #prefixes[i] == #paths[i] and path_separator or "",
95 | ---@diagnostic disable-next-line: param-type-mismatch
96 | fn.join(fn.reverse(prefixes[i]), path_separator),
97 | #prefixes[i] > 0 and path_separator or "",
98 | })
99 | return i, buffer
100 | end)
101 | end
102 |
103 | ---@param filename string
104 | ---@param bufnr bufnr
105 | ---@return string
106 | local get_pick_letter = function(filename, bufnr)
107 | local first_valid = 1
108 |
109 | -- Initialize the valid letters string, if not already initialized
110 | if not valid_pick_letters then
111 | valid_pick_letters = config.pick.letters
112 | end
113 |
114 | -- If the bufnr has already a letter associated to it return that.
115 | if taken_pick_letters[bufnr] then
116 | return taken_pick_letters[bufnr]
117 | end
118 |
119 | -- If the config option pick.use_filename is true, and the initial letter
120 | -- of the filename is valid and it hasn't already been assigned return that.
121 | if config.pick.use_filename and filename ~= "" then
122 | local init_letter = vim.fn.strcharpart(filename, 0, 1) or ""
123 | local idx = valid_pick_letters:find(init_letter, nil, true)
124 |
125 | if idx == nil or taken_pick_indices[idx] then
126 | while
127 | taken_pick_indices[first_valid]
128 | and first_valid <= vim.fn.strcharlen(valid_pick_letters)
129 | do
130 | first_valid = first_valid + 1
131 | end
132 | idx = first_valid
133 | end
134 | if idx then
135 | local letter = vim.fn.strcharpart(valid_pick_letters, idx - 1, 1)
136 | if letter and letter ~= "" then
137 | taken_pick_letters[bufnr] = letter
138 | taken_pick_indices[idx] = true
139 |
140 | return letter
141 | end
142 | end
143 | end
144 |
145 | -- Return the first valid letter if there is one.
146 | while
147 | taken_pick_indices[first_valid]
148 | and first_valid <= vim.fn.strcharlen(valid_pick_letters)
149 | do
150 | first_valid = first_valid + 1
151 | end
152 | if first_valid <= vim.fn.strcharlen(valid_pick_letters) then
153 | local letter = vim.fn.strcharpart(valid_pick_letters, first_valid - 1, 1)
154 | taken_pick_letters[bufnr] = letter
155 | taken_pick_indices[first_valid] = true
156 | return letter
157 | end
158 |
159 | -- Finally, just return a '?' (this is rarely reached, you'd need to have
160 | -- opened 54 buffers in the same session).
161 | return "?"
162 | end
163 |
164 | ---@param path string
165 | ---@param filename string
166 | ---@param buftype string
167 | ---@param filetype string
168 | ---@return table
169 | local get_devicon = function(path, filename, buftype, filetype)
170 | local name = (buftype == "terminal") and "terminal" or filename
171 |
172 | local extn = fn.fnamemodify(path, ":e")
173 | local icon, color =
174 | rq_devicons.get_icon_color(name, extn, { default = false })
175 | if not icon then
176 | icon, color =
177 | rq_devicons.get_icon_color_by_filetype(filetype, { default = true })
178 | end
179 |
180 | return {
181 | icon = icon .. " ",
182 | color = color,
183 | }
184 | end
185 |
186 | ---@param bufnr bufnr
187 | ---@return table
188 | local get_diagnostics = function(bufnr)
189 | local diagnostics = {
190 | errors = 0,
191 | warnings = 0,
192 | infos = 0,
193 | hints = 0,
194 | }
195 |
196 | for _, d in ipairs(diagnostic.get(bufnr)) do
197 | if d.severity == 1 then
198 | diagnostics.errors = diagnostics.errors + 1
199 | elseif d.severity == 2 then
200 | diagnostics.warnings = diagnostics.warnings + 1
201 | elseif d.severity == 3 then
202 | diagnostics.infos = diagnostics.infos + 1
203 | elseif d.severity == 4 then
204 | diagnostics.hints = diagnostics.hints + 1
205 | end
206 | end
207 |
208 | return diagnostics
209 | end
210 |
211 | ---@param b table
212 | ---@return Buffer
213 | Buffer.new = function(b)
214 | local opts = bo[b.bufnr]
215 |
216 | local number = b.bufnr
217 | local buftype = opts.buftype
218 | local path = b.name
219 |
220 | local filename = (type == "quickfix" and "quickfix")
221 | or (#path > 0 and fn.fnamemodify(path, ":t"))
222 | ---@cast filename string
223 |
224 | local filetype = not (b.variables and b.variables.netrw_browser_active)
225 | and opts.filetype
226 | or "netrw"
227 |
228 | local pick_letter = get_pick_letter(filename or "", number)
229 |
230 | local devicon = has_devicons
231 | and get_devicon(path, filename, buftype, filetype)
232 | or { icon = "", color = "" }
233 |
234 | return setmetatable({
235 | _valid_index = buf_order[b.bufnr] or -1,
236 | index = -1,
237 | number = b.bufnr,
238 | type = opts.buftype,
239 | is_focused = (b.bufnr == vim.api.nvim_get_current_buf()),
240 | is_first = false,
241 | is_last = false,
242 | is_modified = opts.modified,
243 | is_readonly = opts.readonly,
244 | is_hovered = false,
245 | buf_hovered = false,
246 | path = b.name,
247 | unique_prefix = "",
248 | filename = filename or "[No Name]",
249 | filetype = filetype,
250 | pick_letter = pick_letter,
251 | devicon = devicon,
252 | diagnostics = get_diagnostics(number),
253 | }, Buffer)
254 | end
255 |
256 | ---@param self Buffer
257 | ---Deletes the buffer
258 | function Buffer:delete()
259 | util.buf_delete(self.number)
260 | vim.cmd.redrawtabline()
261 | end
262 |
263 | ---@param self Buffer
264 | ---Focuses the buffer
265 | function Buffer:focus()
266 | vim.api.nvim_set_current_buf(self.number)
267 | end
268 |
269 | ---@param self Buffer
270 | ---@return number
271 | ---Returns the number of lines in the buffer
272 | function Buffer:lines()
273 | return vim.api.nvim_buf_line_count(self.number)
274 | end
275 |
276 | ---@param self Buffer
277 | ---@return string[]
278 | ---Returns the buffer's lines
279 | function Buffer:text()
280 | return vim.api.nvim_buf_get_lines(self.number, 0, -1, false)
281 | end
282 |
283 | ---@return boolean
284 | ---Returns true if the buffer is valid
285 | function Buffer:is_valid()
286 | return vim.api.nvim_buf_is_valid(self.number)
287 | end
288 |
289 | ---@param buffer Buffer
290 | ---@return boolean
291 | local is_old = function(buffer)
292 | for _, buf in pairs(state.valid_buffers) do
293 | if buffer.number == buf.number then
294 | return true
295 | end
296 | end
297 | return false
298 | end
299 |
300 | ---@param buffer Buffer
301 | ---@return boolean
302 | local is_new = function(buffer)
303 | return not is_old(buffer)
304 | end
305 |
306 | ---Sorter used to open new buffers at the end of the bufferline.
307 | ---@param buffer1 Buffer
308 | ---@param buffer2 Buffer
309 | ---@return boolean
310 | local sort_by_new_after_last = function(buffer1, buffer2)
311 | if is_old(buffer1) and is_old(buffer2) then
312 | return buffer1._valid_index < buffer2._valid_index
313 | elseif is_old(buffer1) and is_new(buffer2) then
314 | return true
315 | elseif is_new(buffer1) and is_old(buffer2) then
316 | return false
317 | else
318 | return buffer1.number < buffer2.number
319 | end
320 | end
321 |
322 | ---Sorter used to open new buffers next to the current buffer.
323 | ---@param buffer1 Buffer
324 | ---@param buffer2 Buffer
325 | ---@return boolean
326 | local sort_by_new_after_current = function(buffer1, buffer2)
327 | if is_old(buffer1) and is_old(buffer2) then
328 | -- If both buffers are either before or after (inclusive) the current
329 | -- buffer, respect the current order.
330 | if
331 | (buffer1._valid_index - current_valid_index)
332 | * (buffer2._valid_index - current_valid_index)
333 | >= 0
334 | then
335 | return buffer1._valid_index < buffer2._valid_index
336 | end
337 | return buffer1._valid_index < current_valid_index
338 | elseif is_old(buffer1) and is_new(buffer2) then
339 | return buffer1._valid_index <= current_valid_index
340 | elseif is_new(buffer1) and is_old(buffer2) then
341 | return current_valid_index < buffer2._valid_index
342 | else
343 | return buffer1.number < buffer2.number
344 | end
345 | end
346 |
347 | ---Sorter used to order buffers by full path.
348 | ---@param buffer1 Buffer
349 | ---@param buffer2 Buffer
350 | ---@return boolean
351 | local sort_by_directory = function(buffer1, buffer2)
352 | return buffer1.path < buffer2.path
353 | end
354 |
355 | ---Sorted used to order buffers by number, as in the default tabline.
356 | ---@param buffer1 Buffer
357 | ---@param buffer2 Buffer
358 | ---@return boolean
359 | local sort_by_number = function(buffer1, buffer2)
360 | return buffer1.number < buffer2.number
361 | end
362 |
363 | ---@param bufnr bufnr
364 | ---@return nil
365 | function M.release_taken_letter(bufnr)
366 | if taken_pick_letters[bufnr] then
367 | local idx = (valid_pick_letters or ""):find(taken_pick_letters[bufnr])
368 | if idx then
369 | taken_pick_indices[idx] = nil
370 | taken_pick_letters[bufnr] = nil
371 | end
372 | end
373 | end
374 |
375 | ---@param buffer Buffer
376 | ---@param target_valid_index valid_index
377 | function M.move_buffer(buffer, target_valid_index)
378 | if buffer._valid_index == target_valid_index then
379 | return
380 | end
381 |
382 | buf_order[buffer.number] = target_valid_index
383 |
384 | if buffer._valid_index < target_valid_index then
385 | for index = (buffer._valid_index + 1), target_valid_index do
386 | buf_order[state.valid_buffers[index].number] = index - 1
387 | end
388 | else
389 | for index = target_valid_index, (buffer._valid_index - 1) do
390 | buf_order[state.valid_buffers[index].number] = index + 1
391 | end
392 | end
393 |
394 | cmd("redrawtabline")
395 | end
396 |
397 | ---@return Buffer[]
398 | function M.get_valid_buffers()
399 | if not buf_order then
400 | buf_order = {}
401 | end
402 |
403 | local info = fn.getbufinfo({ buflisted = 1 })
404 | ---@type Iterator|table
405 | local buffers = iter(info):map(Buffer.new):filter(function(buffer)
406 | return buffer.filetype ~= "netrw"
407 | end)
408 |
409 | if config.buffers.filter_valid then
410 | ---@type Iterator|table
411 | buffers = buffers:filter(config.buffers.filter_valid)
412 | end
413 |
414 | ---@type Iterator|table
415 | buffers = compute_unique_prefixes(buffers)
416 |
417 | if current_valid_index == nil then
418 | buf_order = {}
419 | buffers = buffers
420 | :map(function(i, buffer)
421 | buffer._valid_index = i
422 | buf_order[buffer.number] = buffer._valid_index
423 | if buffer.is_focused then
424 | current_valid_index = i
425 | end
426 | return buffer
427 | end)
428 | :tolist()
429 | else
430 | buffers = buffers
431 | :map(function(_, buf)
432 | return buf
433 | end)
434 | :tolist()
435 | end
436 | if not current_valid_index then
437 | current_valid_index = 0
438 | end
439 |
440 | if type(config.buffers.new_buffers_position) == "function" then
441 | sort(buffers, config.buffers.new_buffers_position)
442 | elseif config.buffers.new_buffers_position == "last" then
443 | sort(buffers, sort_by_new_after_last)
444 | elseif config.buffers.new_buffers_position == "next" then
445 | sort(buffers, sort_by_new_after_current)
446 | elseif config.buffers.new_buffers_position == "directory" then
447 | sort(buffers, sort_by_directory)
448 | elseif config.buffers.new_buffers_position == "number" then
449 | sort(buffers, sort_by_number)
450 | end
451 |
452 | buf_order = {}
453 | for i, buffer in ipairs(buffers) do
454 | buffer._valid_index = i
455 | buf_order[buffer.number] = buffer._valid_index
456 | if buffer.is_focused then
457 | current_valid_index = i
458 | end
459 | end
460 |
461 | return buffers
462 | end
463 |
464 | ---@return Buffer[]
465 | function M.get_visible()
466 | state.valid_buffers = M.get_valid_buffers()
467 | state.valid_lookup = {}
468 |
469 | local bufs = iter(state.valid_buffers):map(function(buffer)
470 | state.valid_lookup[buffer.number] = buffer
471 | return buffer
472 | end)
473 |
474 | if config.buffers.filter_visible then
475 | bufs = bufs:filter(config.buffers.filter_visible)
476 | end
477 |
478 | bufs = bufs:enumerate():map(function(i, buf)
479 | buf.index = i
480 | return buf
481 | end)
482 |
483 | if not config.pick.use_filename then
484 | bufs = bufs:map(function(buf)
485 | buf.pick_letter = get_pick_letter(buf.filename, buf.number)
486 | return buf
487 | end)
488 | end
489 |
490 | state.visible_buffers = bufs:tolist()
491 |
492 | if #state.visible_buffers > 0 then
493 | state.visible_buffers[1].is_first = true
494 | state.visible_buffers[#state.visible_buffers].is_last = true
495 | end
496 |
497 | return state.visible_buffers
498 | end
499 |
500 | ---Get Buffer
501 | ---@param bufnr number
502 | ---@return Buffer|nil
503 | function M.get_buffer(bufnr)
504 | return require("cokeline.state").valid_lookup[bufnr]
505 | end
506 |
507 | ---Wrapper around `vim.api.nvim_get_current_buf`, returns Buffer object
508 | ---@return Buffer|nil
509 | function M.get_current()
510 | local bufnr = vim.api.nvim_get_current_buf()
511 | if bufnr then
512 | return M.get_buffer(bufnr)
513 | end
514 | end
515 |
516 | ---@param bufnr bufnr
517 | ---@return boolean
518 | function M.is_visible(bufnr)
519 | local buf = M.get_buffer(bufnr)
520 | if not buf then
521 | return false
522 | end
523 | if config.buf.filter_valid and not config.buffers.filter_valid(buf) then
524 | return false
525 | end
526 | if
527 | config.buffers.filter_visible
528 | and not config.buffers.filter_visible(buf)
529 | then
530 | return false
531 | end
532 | return true
533 | end
534 |
535 | M.Buffer = Buffer
536 |
537 | return M
538 |
--------------------------------------------------------------------------------
/lua/cokeline/components.lua:
--------------------------------------------------------------------------------
1 | local lazy = require("cokeline.lazy")
2 | local Hlgroup = require("cokeline.hlgroups").Hlgroup
3 | local config = lazy("cokeline.config")
4 |
5 | local rep = string.rep
6 | local insert = table.insert
7 | local remove = table.remove
8 |
9 | local iter = require("plenary.iterators").iter
10 | local fn = vim.fn
11 |
12 | ---@generic Cx
13 | ---@class Component
14 | ---@field index number
15 | ---@field text string|fun(cx: Cx): string
16 | ---@field style string|fun(cx: Cx): string
17 | ---@field fg string|fun(cx: Cx): string
18 | ---@field bg string|fun(cx: Cx): string
19 | ---@field highlight string|fun(cx: Cx): string
20 | ---@field on_click ClickHandler | nil
21 | ---@field on_mouse_enter MouseEnterHandler | nil
22 | ---@field on_mouse_leave MouseLeaveHandler | nil
23 | ---@field delete_buffer_on_left_click boolean Use the component as a close button ()
24 | ---@field truncation table
25 | ---@field idx number
26 | ---@field bufnr bufnr
27 | ---@field width number
28 | ---@field hlgroup Hlgroup
29 | local Component = {}
30 | Component.__index = Component
31 |
32 | ---@generic Cx
33 | ---@param c table | Component
34 | ---@param i number
35 | ---@return Component
36 | Component.new = function(c, i, default_hl)
37 | local function attr(name, default)
38 | if c[name] ~= nil then
39 | return c[name]
40 | end
41 | if default_hl[name] ~= nil then
42 | return default_hl[name]
43 | end
44 | return default
45 | end
46 | -- `default_hl` is `nil` when called by `components.lua#63`
47 | default_hl = default_hl or config.default_hl
48 | local component = {
49 | index = i,
50 | text = c.text,
51 | fg = attr("fg", "NONE"),
52 | bg = attr("bg", "NONE"),
53 | sp = attr("sp", "NONE"),
54 | bold = attr("bold"),
55 | italic = attr("italic"),
56 | underline = attr("underline"),
57 | undercurl = attr("undercurl"),
58 | strikethrough = attr("strikethrough"),
59 | highlight = c.highlight,
60 | delete_buffer_on_left_click = c.delete_buffer_on_left_click or false,
61 | on_click = c.on_click,
62 | on_mouse_enter = c.on_mouse_enter,
63 | on_mouse_leave = c.on_mouse_leave,
64 | truncation = {
65 | priority = c.truncation and c.truncation.priority or i,
66 | direction = c.truncation and c.truncation.direction or "right",
67 | },
68 | -- These values aren't ready yet, they will be once the component gets
69 | -- rendered for a specific buffer.
70 | width = nil,
71 | bufnr = nil,
72 | hlgroup = nil,
73 | kind = c.kind or "buffer",
74 | }
75 | setmetatable(component, Component)
76 | return component
77 | end
78 |
79 | -- Renders a component for a specific buffer.
80 | ---@generic Cx
81 | ---@param self Component
82 | ---@param context RenderContext
83 | ---@return Component
84 | Component.render = function(self, context)
85 | ---@return string
86 | local evaluate = function(field)
87 | if field == nil then
88 | return
89 | end
90 | if type(field) == "function" then
91 | return field(context.provider)
92 | end
93 | return field
94 | end
95 |
96 | local component = vim.deepcopy(self)
97 | component.text = evaluate(self.text)
98 | component.width = fn.strwidth(component.text)
99 |
100 | if
101 | context.kind == "buffer"
102 | or context.kind == "sidebar"
103 | or context.kind == "tab"
104 | then
105 | component.bufnr = context.provider.number
106 | end
107 |
108 | if component.highlight then
109 | component.hlgroup = Hlgroup.new_existing(evaluate(component.highlight))
110 | else
111 | -- `evaluate(self.hl.*)` might return `nil`, in that case we fallback to the
112 | -- default highlight first and to NONE if that's `nil` too.
113 | -- local style = evaluate(self.style)
114 | -- or evaluate(config.default_hl.style)
115 | -- or "NONE"
116 | local fg = evaluate(self.fg) or evaluate(config.default_hl.fg) or "NONE"
117 | local bg = evaluate(self.bg) or evaluate(config.default_hl.bg) or "NONE"
118 | local sp = evaluate(self.sp) or evaluate(config.default_hl.sp) or "NONE"
119 | local attrs = {}
120 | attrs.bold = evaluate(self.bold) or evaluate(config.default_hl.bold)
121 | attrs.italic = evaluate(self.italic) or evaluate(config.default_hl.italic)
122 | attrs.underline = evaluate(self.underline)
123 | or evaluate(config.default_hl.underline)
124 | attrs.undercurl = evaluate(self.undercurl)
125 | or evaluate(config.default_hl.undercurl)
126 | attrs.strikethrough = evaluate(self.strikethrough)
127 | or evaluate(config.default_hl.strikethrough)
128 |
129 | component.hlgroup = Hlgroup.new(
130 | ("Cokeline_%s_%s"):format(component.bufnr or context.kind, self.index),
131 | fg,
132 | bg,
133 | sp,
134 | attrs
135 | )
136 | end
137 |
138 | return component
139 | end
140 |
141 | ---@generic Cx
142 | ---@param self Component
143 | ---@param to_width number
144 | ---@param direction '"left"' | '"right"' | nil
145 | Component.shorten = function(self, to_width, direction)
146 | -- If a direction is given that means we're cutting off a component that's
147 | -- at the edge of the bufferline (either the left or the right one). In this
148 | -- case we either prepend or append a space to the ellipses.
149 | --
150 | -- If a direction isn't given we're shortening a component within a buffer.
151 | -- Here we use that component's `truncation.direction`, and we don't add any
152 | -- additional spaces.
153 | if direction then
154 | local available_width = to_width - 2
155 | local start = direction == "left" and self.width - available_width or 0
156 | self.text = (direction == "left" and " …%s" or "%s… "):format(
157 | fn.strcharpart(self.text, start, available_width)
158 | )
159 | elseif self.truncation.direction == "middle" then
160 | local available_width = to_width - 1
161 | local width_left = math.floor(available_width / 2)
162 | local width_right = width_left + available_width % 2
163 | self.text = ("%s…%s"):format(
164 | fn.strcharpart(self.text, 0, width_left),
165 | fn.strcharpart(self.text, self.width - width_right, width_right)
166 | )
167 | else
168 | direction = self.truncation.direction
169 | local available_width = to_width - 1
170 | local start = direction == "left" and self.width - available_width or 0
171 | self.text = (direction == "left" and "…%s" or "%s…"):format(
172 | fn.strcharpart(self.text, start, available_width)
173 | )
174 | end
175 |
176 | -- `fn.strcharpart` can fail with wide characters. For example,
177 | -- `fn.strcharpart("|", 0, 1)` will still return "|" since that character
178 | -- takes up two columns. This is to handle such cases.
179 | if fn.strwidth(self.text) ~= to_width then
180 | local fmt = (direction == "left" and "%s…")
181 | or (direction == "right" and "…%s")
182 | or "%s "
183 | self.text = fmt:format(rep(" ", to_width ~= nil and to_width - 1 or 0))
184 | end
185 |
186 | self.width = fn.strwidth(self.text)
187 | end
188 |
189 | ---@param components Component[]
190 | ---@return number
191 | local width_of_components = function(components)
192 | return iter(components):fold(0, function(a, c)
193 | return a + c.width
194 | end)
195 | end
196 |
197 | -- Takes a list of components, returns a new list of components `to_width` wide
198 | -- obtained by trimming the original `components` in the given `direction`.
199 | ---@generic Cx
200 | ---@param components Component[]
201 | ---@param to_width number
202 | ---@param direction '"left"' | '"right"' | nil
203 | ---@return Component[]
204 | local shorten_components = function(components, to_width, direction)
205 | if #components == 0 then return components end
206 |
207 | local current_width = width_of_components(components)
208 |
209 | -- `extra` is the width of the extra characters that are appended when a
210 | -- component is shortened, an ellipses if we're shortening within a buffer
211 | -- (in which case the `direction` is `nil`), or an ellipses and a space if
212 | -- we're cutting off a component at the edge of the bufferline (where
213 | -- `direction` is either `"left"` or `"right"`).
214 | local extra = direction and 2 or 1
215 | local i = direction == "left" and 1 or #components
216 | -- We start from the full list of components and remove components as
217 | -- necessary. We could start from an empty list and add until there's space,
218 | -- but I think there's usually more components to keep than to throw away, so
219 | -- this should be faster in most cases.
220 | local last_removed_component
221 | while
222 | (current_width > to_width)
223 | or (current_width - components[i].width + extra > to_width)
224 | do
225 | local removed = remove(components, i)
226 | if removed then
227 | last_removed_component = removed
228 | current_width = current_width - last_removed_component.width
229 | i = direction == "left" and 1 or i - 1
230 | if #components == 0 then
231 | break
232 | end
233 | else
234 | break
235 | end
236 | end
237 |
238 | if
239 | last_removed_component
240 | and (to_width - current_width > extra or #components == 0)
241 | then
242 | i = direction == "left" and 1 or i + 1
243 | last_removed_component:shorten(to_width - current_width, direction)
244 | insert(components, i, last_removed_component)
245 | else
246 | -- Here we "shorten" a component to more than its current width.
247 | components[i]:shorten(
248 | components[i].width + to_width - current_width,
249 | direction
250 | )
251 | end
252 |
253 | return components
254 | end
255 |
256 | -- Takes in a list of components, returns the concatenation of their rendered
257 | -- strings.
258 | ---@generic Cx
259 | ---@param components Component[]
260 | ---@return string
261 | local render_components = function(components)
262 | local embed = function(component)
263 | local text = component.hlgroup:embed(component.text)
264 | if not fn.has("tablineat") then
265 | return text
266 | else
267 | local on_click
268 | if component.delete_buffer_on_left_click then
269 | on_click = string.format(
270 | "v:lua.require'cokeline.handlers'.close_%d",
271 | component.bufnr
272 | )
273 | else
274 | on_click = string.format(
275 | "v:lua.require'cokeline.handlers'.click_%s_%d_%d",
276 | component.kind,
277 | component.index,
278 | component.bufnr or 0
279 | )
280 | end
281 | return ("%%%s@%s@%s%%X"):format(component.index, on_click, text)
282 | end
283 | end
284 |
285 | local concat = function(a, b)
286 | return a .. b
287 | end
288 |
289 | return iter(components):map(embed):fold("", concat)
290 | end
291 |
292 | return {
293 | Component = Component,
294 | render = render_components,
295 | shorten = shorten_components,
296 | width = width_of_components,
297 | }
298 |
--------------------------------------------------------------------------------
/lua/cokeline/config.lua:
--------------------------------------------------------------------------------
1 | local lazy = require("cokeline.lazy")
2 | ---@module cokeline.state
3 | local state = lazy("cokeline.state")
4 | ---@module "cokeline.sliders"
5 | local sliders = lazy("cokeline.sliders")
6 | ---@module "cokeline.hlgroups"
7 | local hlgroups = lazy("cokeline.hlgroups")
8 |
9 | local insert = table.insert
10 |
11 | local echo = vim.api.nvim_echo
12 | local islist = vim.islist or vim.tbl_islist
13 |
14 | local defaults = {
15 | show_if_buffers_are_at_least = 1,
16 |
17 | buffers = {
18 | filter_valid = false,
19 | filter_visible = false,
20 | focus_on_delete = "next",
21 | new_buffers_position = "last",
22 | delete_on_right_click = true,
23 | },
24 |
25 | mappings = {
26 | cycle_prev_next = true,
27 | disable_mouse = false,
28 | },
29 |
30 | history = {
31 | enabled = true,
32 | size = 2,
33 | },
34 |
35 | rendering = {
36 | max_buffer_width = 999,
37 | slider = sliders.center_current_buffer,
38 | },
39 |
40 | pick = {
41 | use_filename = true,
42 | letters = "asdfjkl;ghnmxcvbziowerutyqpASDFJKLGHNMXCVBZIOWERUTYQP",
43 | },
44 |
45 | default_hl = {
46 | fg = function(buffer)
47 | return buffer.is_focused and "TabLineSel" or "TabLine"
48 | end,
49 | bg = function(buffer)
50 | return buffer.is_focused and "TabLineSel" or "TabLine"
51 | end,
52 | },
53 |
54 | fill_hl = "TabLineFill",
55 |
56 | components = {
57 | {
58 | text = function(buffer)
59 | return " " .. buffer.devicon.icon
60 | end,
61 | fg = function(buffer)
62 | return buffer.devicon.color
63 | end,
64 | },
65 | {
66 | text = function(buffer)
67 | return buffer.unique_prefix
68 | end,
69 | fg = function()
70 | return hlgroups.get_hl_attr("Comment", "fg")
71 | end,
72 | italic = true,
73 | },
74 | {
75 | text = function(buffer)
76 | return buffer.filename
77 | end,
78 | underline = function(buffer)
79 | if buffer.is_hovered and not buffer.is_focused then
80 | return true
81 | end
82 | end,
83 | },
84 | {
85 | text = " ",
86 | },
87 | {
88 | ---@param buffer Buffer
89 | text = function(buffer)
90 | if buffer.is_modified then
91 | return ""
92 | end
93 | if buffer.is_hovered then
94 | return ""
95 | end
96 | return ""
97 | end,
98 | on_click = function(_, _, _, _, buffer)
99 | buffer:delete()
100 | end,
101 | },
102 | {
103 | text = " ",
104 | },
105 | },
106 |
107 | tabs = {
108 | placement = "right",
109 | components = {},
110 | },
111 |
112 | rhs = {},
113 |
114 | sidebar = {
115 | filetype = { "NvimTree", "neo-tree", "SidebarNvim" },
116 | components = {},
117 | },
118 | }
119 |
120 | -- Formats an error message.
121 | ---@param msg string
122 | local echoerr = function(msg)
123 | echo({
124 | { "[nvim-cokeline]: ", "ErrorMsg" },
125 | { 'Configuration option "' },
126 | { msg, "WarningMsg" },
127 | { '" does not exist!' },
128 | }, true, {})
129 | end
130 |
131 | local config = vim.deepcopy(defaults)
132 |
133 | -- Updates the `settings` table with options from `preferences`, printing an
134 | -- error message if a configuration option in `preferences` is not defined in
135 | -- `settings`.
136 | ---@param settings table
137 | ---@param preferences table
138 | ---@param key string | nil
139 | ---@return table
140 | local function update(settings, preferences, key)
141 | local updated = settings
142 | for k, v in pairs(preferences) do
143 | local key_tree = key and ("%s.%s"):format(key, k) or k
144 | if settings[k] == nil and key ~= "default_hl" then
145 | echoerr(key_tree)
146 | elseif type(v) == "table" and not islist(v) then
147 | updated[k] = update(settings[k], v, key_tree)
148 | else
149 | updated[k] = v
150 | end
151 | end
152 | return updated
153 | end
154 |
155 | local setup = function(opts)
156 | config = update(defaults, opts)
157 | state.components = {}
158 | state.rhs = {}
159 | state.sidebar = {}
160 | state.tabs = {}
161 | local Component = lazy("cokeline.components").Component
162 | if opts.buffers and opts.buffers.new_buffers_position then
163 | if not config.buffers then
164 | config.buffers = {}
165 | end
166 | config.buffers.new_buffers_position = opts.buffers.new_buffers_position
167 | end
168 | local id = 1
169 | for _, component in ipairs(config.components) do
170 | local new_component = Component.new(component, id, opts.default_hl)
171 | insert(state.components, new_component)
172 | if new_component.on_click ~= nil then
173 | require("cokeline.handlers").click:register(id, new_component.on_click)
174 | end
175 | id = id + 1
176 | end
177 | if opts.rhs then
178 | for _, component in ipairs(config.rhs) do
179 | component.kind = "rhs"
180 | local new_component = Component.new(component, id, opts.default_hl)
181 | insert(state.rhs, new_component)
182 | if new_component.on_click ~= nil then
183 | require("cokeline.handlers").click:register(id, new_component.on_click)
184 | end
185 | id = id + 1
186 | end
187 | end
188 | if config.sidebar and config.sidebar.components then
189 | for _, component in ipairs(config.sidebar.components) do
190 | component.kind = "sidebar"
191 | local new_component = Component.new(component, id, opts.default_hl)
192 | insert(state.sidebar, new_component)
193 | if new_component.on_click ~= nil then
194 | require("cokeline.handlers").click:register(id, new_component.on_click)
195 | end
196 | id = id + 1
197 | end
198 | end
199 | if config.tabs and config.tabs.components then
200 | for _, component in ipairs(config.tabs.components) do
201 | component.kind = "tab"
202 | local new_component = Component.new(component, id, opts.default_hl)
203 | insert(state.tabs, new_component)
204 | if new_component.on_click ~= nil then
205 | require("cokeline.handlers").click:register(id, new_component.on_click)
206 | end
207 | id = id + 1
208 | end
209 | end
210 | return config
211 | end
212 |
213 | return setmetatable({
214 | setup = setup,
215 | get = function()
216 | return config
217 | end,
218 | }, {
219 | __index = function(_, k)
220 | return config[k]
221 | end,
222 | __newindex = function(_, k, v)
223 | config[k] = v
224 | end,
225 | })
226 |
--------------------------------------------------------------------------------
/lua/cokeline/context.lua:
--------------------------------------------------------------------------------
1 | ---@alias ContextKind "buffer" | "tab" | "rhs" | "sidebar"
2 |
3 | ---@class RenderContext
4 | ---@field kind ContextKind
5 | ---@field provider Buffer | TabPage | RhsContext
6 | local RenderContext = {}
7 |
8 | ---@param tab TabPage
9 | function RenderContext:tab(tab)
10 | return {
11 | provider = tab,
12 | kind = "tab",
13 | }
14 | end
15 |
16 | ---@param rhs RhsContext
17 | function RenderContext:rhs(rhs)
18 | return {
19 | provider = rhs,
20 | kind = "rhs",
21 | }
22 | end
23 |
24 | ---@param buffer Buffer
25 | function RenderContext:buffer(buffer)
26 | return {
27 | provider = buffer,
28 | kind = "buffer",
29 | }
30 | end
31 |
32 | return RenderContext
33 |
--------------------------------------------------------------------------------
/lua/cokeline/handlers.lua:
--------------------------------------------------------------------------------
1 | local lazy = require("cokeline.lazy")
2 | local config = lazy("cokeline.config")
3 | local utils = lazy("cokeline.utils")
4 | local buffers = lazy("cokeline.buffers")
5 | local tabs = lazy("cokeline.tabs")
6 |
7 | ---@alias ClickHandler fun(button_id: number, clicks: number, button: string, modifiers: string, cx: table)
8 | ---@alias MouseEnterHandler fun(cx: table)
9 | ---@alias MouseLeaveHandler fun(cx: table)
10 | ---@alias Handler ClickHandler | MouseEnterHandler | MouseLeaveHandler
11 | ---@alias WrappedHandler fun(cx: table): Handler
12 |
13 | ---@class Handlers Singleton event handler manager
14 | ---@field click (fun(cx: table): Handler)[]
15 | ---@field private private table
16 | local Handlers = {
17 | click = {},
18 | private = {
19 | click = {},
20 | },
21 | }
22 |
23 | ---@param kind string
24 | ---@param idx number
25 | local function unregister(kind, idx)
26 | ---@diagnostic disable: cast-local-type
27 | if idx == nil then
28 | idx = kind
29 | kind = nil
30 | end
31 | rawset(Handlers.private[kind], idx, nil)
32 | end
33 |
34 | ---Register a click handler
35 | ---@param idx number
36 | ---@param handler ClickHandler
37 | function Handlers.click:register(idx, handler)
38 | rawset(Handlers.private.click, idx, function(buffer)
39 | return function(minwid, clicks, button, modifiers)
40 | return handler(minwid, clicks, button, modifiers, buffer)
41 | end
42 | end)
43 | end
44 |
45 | ---Unregister a click handler
46 | ---@param idx number
47 | function Handlers.click:unregister(idx)
48 | unregister("click", idx)
49 | end
50 |
51 | ---Default click handler
52 | ---@param cx table
53 | ---@param kind string
54 | local function default_click(cx, kind)
55 | return function(_, _, button)
56 | if button == "l" then
57 | if kind == "tab" or kind == "buffer" then
58 | cx:focus()
59 | end
60 | elseif
61 | kind == "buffer"
62 | and button == "r"
63 | and config.buffers.delete_on_right_click
64 | then
65 | utils.buf_delete(cx.number, config.buffers.focus_on_delete)
66 | end
67 | end
68 | end
69 |
70 | ---Default close handler
71 | ---@param buffer bufnr
72 | local function default_close(buffer)
73 | return function(_, _, button)
74 | if button == "l" then
75 | utils.buf_delete(buffer.number, config.buffers.focus_on_delete)
76 | end
77 | end
78 | end
79 |
80 | --- Public handlers interface
81 | return setmetatable({}, {
82 | __index = function(_, key)
83 | --- Retrieve a helper for registering events
84 | local helper = rawget(Handlers, key)
85 | if helper ~= nil then
86 | return key ~= "private" and helper or nil
87 | end
88 |
89 | --- If there's no helper, evaluate the key to get a handler
90 | local prefix = string.sub(key, 1, 5)
91 | if prefix == "click" then
92 | local kind, id, number = string.match(key, "click_(%a+)_(%d+)_(%d+)")
93 | if number == nil then
94 | return
95 | end
96 | local cx
97 | if kind == "tab" then
98 | cx = tabs.get_tabpage(tonumber(number))
99 | else
100 | cx = buffers.get_buffer(tonumber(number))
101 | end
102 |
103 | if id ~= nil then
104 | local handler = Handlers.private["click"][tonumber(id)]
105 | return handler and handler(cx) or default_click(cx, kind)
106 | else
107 | return default_click(cx, kind)
108 | end
109 | elseif prefix == "close" then
110 | local bufnr = string.match(key, "close_(%d+)")
111 | if bufnr then
112 | local buffer = buffers.get_buffer(tonumber(bufnr))
113 | return default_close(buffer)
114 | end
115 | end
116 | return nil
117 | end,
118 | __newindex = function()
119 | vim.api.nvim_err_writeln("Cannot set fields in cokeline.handlers")
120 | end,
121 | })
122 |
--------------------------------------------------------------------------------
/lua/cokeline/history.lua:
--------------------------------------------------------------------------------
1 | local lazy = require("cokeline.lazy")
2 | local buffers = lazy("cokeline.buffers")
3 |
4 | ---@class Cokeline.History
5 | ---@field _len integer
6 | ---@field _cap integer
7 | ---@field _data bufnr[]
8 | local History = {
9 | _len = 0,
10 | _cap = 4,
11 | _data = {},
12 | _read = 1,
13 | _write = 1,
14 | }
15 |
16 | ---Creates a new History object
17 | ---@return Cokeline.History
18 | function History.setup(cap)
19 | History._cap = cap
20 | return History
21 | end
22 |
23 | ---Adds a Buffer object to the history
24 | ---@param bufnr bufnr
25 | function History:push(bufnr)
26 | self._data[self._write] = bufnr
27 | self._write = (self._write % self._cap) + 1
28 | if self._len < self._cap then
29 | self._len = self._len + 1
30 | end
31 | end
32 |
33 | ---Removes and returns the oldest Buffer object in the history
34 | ---@return Buffer|nil
35 | function History:pop()
36 | if self._len == 0 then
37 | return nil
38 | end
39 | local bufnr = self._data[self._read]
40 | self._data[self._read] = nil
41 | self._read = (self._read % self._cap) + 1
42 | if bufnr then
43 | self._len = self._len - 1
44 | end
45 | if bufnr then
46 | return buffers.get_buffer(bufnr)
47 | end
48 | end
49 |
50 | ---Returns a list of Buffer objects in the history,
51 | ---ordered from oldest to newest
52 | ---@return Buffer[]
53 | function History:list()
54 | local list = {}
55 | local read = self._read
56 | for _ = 1, self._len do
57 | local buf = self._data[read]
58 | if buf then
59 | table.insert(list, buffers.get_buffer(buf))
60 | end
61 | read = (read % self._cap) + 1
62 | end
63 | return list
64 | end
65 |
66 | ---Returns an iterator of Buffer objects in the history,
67 | ---ordered from oldest to newest
68 | ---@return fun():Buffer?
69 | function History:iter()
70 | local read = self._read
71 | return function()
72 | local buf = self._data[read]
73 | if buf then
74 | read = read + 1
75 | return buffers.get_buffer(buf)
76 | end
77 | end
78 | end
79 |
80 | ---Get a Buffer object by history index
81 | ---@param idx integer
82 | ---@return Buffer|nil
83 | function History:get(idx)
84 | local buf = self._data[(self._read % self._cap) + idx + 1]
85 | if buf then
86 | return buffers.get_buffer(buf)
87 | end
88 | end
89 |
90 | ---Get a Buffer object representing the last-accessed buffer (before the current one)
91 | ---@return Buffer|nil
92 | function History:last()
93 | local buf = self._data[self._write == 1 and self._len or (self._write - 1)]
94 | if buf then
95 | return buffers.get_buffer(buf)
96 | end
97 | end
98 |
99 | ---Returns true if the history is empty
100 | ---@return boolean
101 | function History:is_empty()
102 | return self._read == self._write
103 | end
104 |
105 | ---Returns the maximum number of buffers that can be stored in the history
106 | ---@return integer
107 | function History:capacity()
108 | return self._cap
109 | end
110 |
111 | ---Returns true if the history contains the given buffer
112 | ---@param bufnr bufnr
113 | ---@return boolean
114 | function History:contains(bufnr)
115 | return vim.tbl_contains(self._data, bufnr)
116 | end
117 |
118 | ---Returns the number of buffers in the history
119 | ---@return integer
120 | function History:len()
121 | return self._len
122 | end
123 |
124 | return History
125 |
--------------------------------------------------------------------------------
/lua/cokeline/hlgroups.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 |
3 | -- These functions are called a LOT.
4 | -- Cache the results to avoid repeat API calls
5 | local cache = {
6 | hex = {},
7 | groups = {},
8 | }
9 |
10 | ---Clears the hlgroup cache.
11 | ---@private
12 | function M._cache_clear()
13 | cache.groups = {}
14 | end
15 |
16 | ---@param rgb integer
17 | ---@return string hex
18 | function M.hex(rgb)
19 | if cache.hex[rgb] then
20 | return cache.hex[rgb]
21 | end
22 | local band, lsr = bit.band, bit.rshift
23 |
24 | local r = lsr(band(rgb, 0xff0000), 16)
25 | local g = lsr(band(rgb, 0x00ff00), 8)
26 | local b = band(rgb, 0x0000ff)
27 |
28 | local res = ("#%02x%02x%02x"):format(r, g, b)
29 | cache.hex[rgb] = res
30 | return res
31 | end
32 |
33 | function M.get_hl(name)
34 | if cache.groups[name] then
35 | return cache.groups[name]
36 | end
37 | local hl = vim.api.nvim_get_hl(0, { name = name })
38 | if not hl then
39 | return
40 | end
41 | if hl.fg and type(hl.fg) == "number" then
42 | hl.fg = M.hex(hl.fg)
43 | end
44 | if hl.bg and type(hl.bg) == "number" then
45 | hl.bg = M.hex(hl.bg)
46 | end
47 | if hl.sp and type(hl.sp) == "number" then
48 | hl.sp = M.hex(hl.sp)
49 | end
50 | cache.groups[name] = hl
51 | return hl
52 | end
53 |
54 | function M.get_hl_attr(name, attr)
55 | if cache.groups[name] and cache.groups[name][attr] then
56 | return cache.groups[name][attr]
57 | end
58 | local hl = M.get_hl(name)
59 | if not hl then
60 | return
61 | end
62 | return hl[attr]
63 | end
64 |
65 | ---@class Hlgroup
66 | ---@field name string
67 | ---@field gui string
68 | ---@field guifg string
69 | ---@field guibg string
70 | local Hlgroup = {}
71 | Hlgroup.__index = Hlgroup
72 |
73 | ---Sets the highlight group, then returns a new `Hlgroup` table.
74 | ---@param name string
75 | ---@param fg string
76 | ---@param bg string | nil
77 | ---@param sp string | nil
78 | ---@param gui_attrs table
79 | ---@return Hlgroup
80 | Hlgroup.new = function(name, fg, bg, sp, gui_attrs)
81 | local hlgroup = {}
82 | if fg ~= "NONE" and vim.fn.hlexists(fg) == 1 then
83 | hlgroup.fg = M.get_hl_attr(fg, "fg")
84 | else
85 | hlgroup.fg = fg or "NONE"
86 | end
87 | if bg ~= "NONE" and vim.fn.hlexists(bg) == 1 then
88 | hlgroup.bg = M.get_hl_attr(bg, "bg")
89 | else
90 | hlgroup.bg = bg or "NONE"
91 | end
92 | if sp and sp ~= "NONE" and vim.fn.hlexists(sp) == 1 then
93 | hlgroup.sp = M.get_hl_attr(sp, "sp")
94 | else
95 | hlgroup.sp = sp or "NONE"
96 | end
97 |
98 | for attr, val in pairs(gui_attrs) do
99 | if type(val) == "boolean" then
100 | hlgroup[attr] = val
101 | end
102 | end
103 |
104 | hlgroup.default = false
105 | vim.api.nvim_set_hl(0, name, hlgroup)
106 |
107 | hlgroup.name = name
108 | setmetatable(hlgroup, Hlgroup)
109 | return hlgroup
110 | end
111 |
112 | ---Using already existing highlight group, returns a new `Hlgroup` table.
113 | ---@param name string
114 | ---@return Hlgroup
115 | Hlgroup.new_existing = function(name)
116 | local hlgroup = {
117 | name = name,
118 | }
119 | setmetatable(hlgroup, Hlgroup)
120 | return hlgroup
121 | end
122 |
123 | ---Embeds some text in a highlight group.
124 | ---@param self Hlgroup
125 | ---@param text string
126 | ---@return string
127 | Hlgroup.embed = function(self, text)
128 | return ("%%#%s#%s%%*"):format(self.name, text)
129 | end
130 |
131 | M.Hlgroup = Hlgroup
132 |
133 | return M
134 |
--------------------------------------------------------------------------------
/lua/cokeline/hover.lua:
--------------------------------------------------------------------------------
1 | local lazy = require("cokeline.lazy")
2 | local M = {}
3 |
4 | local version = vim.version()
5 |
6 | local buffers = lazy("cokeline.buffers")
7 | local config = lazy("cokeline.config")
8 | local tabs = lazy("cokeline.tabs")
9 | local rendering = lazy("cokeline.rendering")
10 | local iter = lazy("plenary.iterators").iter
11 | local last_position = nil
12 |
13 | local hovered
14 | local dragging
15 |
16 | function M.hovered()
17 | return hovered
18 | end
19 |
20 | function M.set_hovered(val)
21 | hovered = val
22 | end
23 |
24 | function M.clear_hovered()
25 | hovered = nil
26 | end
27 |
28 | function M.dragging()
29 | return dragging
30 | end
31 |
32 | function M.set_dragging(val)
33 | dragging = val
34 | end
35 |
36 | function M.clear_dragging()
37 | dragging = nil
38 | end
39 |
40 | function M.get_current(col)
41 | local bufs = buffers.get_visible()
42 | if not bufs then
43 | return
44 | end
45 | local cx = rendering.prepare(bufs)
46 |
47 | local current_width = 0
48 | for _, component in ipairs(cx.sidebar_left) do
49 | current_width = current_width + component.width
50 | if current_width >= col then
51 | return component, cx.sidebar_left
52 | end
53 | end
54 | if config.tabs and config.tabs.placement == "left" then
55 | for _, component in ipairs(cx.tabs) do
56 | current_width = current_width + component.width
57 | if current_width >= col then
58 | return component, cx.tabs
59 | end
60 | end
61 | end
62 | for _, component in ipairs(cx.buffers) do
63 | current_width = current_width + component.width
64 | if current_width >= col then
65 | return component, cx.buffers
66 | end
67 | end
68 | current_width = current_width + cx.gap
69 | if current_width >= col then
70 | return
71 | end
72 | for _, component in ipairs(cx.rhs) do
73 | current_width = current_width + component.width
74 | if current_width >= col then
75 | return component, cx.rhs
76 | end
77 | end
78 | if config.tabs and config.tabs.placement == "right" then
79 | for _, component in ipairs(cx.tabs) do
80 | current_width = current_width + component.width
81 | if current_width >= col then
82 | return component, cx.tabs
83 | end
84 | end
85 | end
86 | for _, component in ipairs(cx.sidebar_right) do
87 | current_width = current_width + component.width
88 | if current_width >= col then
89 | return component, cx.sidebar_right
90 | end
91 | end
92 | end
93 |
94 | local function mouse_leave(component)
95 | local cx
96 | if component.kind == "buffer" then
97 | cx = buffers.get_buffer(component.bufnr)
98 | elseif component.kind == "tab" then
99 | cx = tabs.get_tabpage(component.bufnr)
100 | elseif component.kind == "sidebar" then
101 | cx = { number = component.bufnr, side = component.sidebar }
102 | else
103 | cx = {}
104 | end
105 | if cx then
106 | cx.is_hovered = false
107 | end
108 | if component.on_mouse_leave then
109 | if
110 | (component.kind ~= "buffer" and component.kind ~= "tab") or cx ~= nil
111 | then
112 | component.on_mouse_leave(cx)
113 | end
114 | end
115 | M.clear_hovered()
116 | end
117 |
118 | local function mouse_enter(component, current)
119 | local cx
120 | if component.kind == "buffer" then
121 | cx = buffers.get_buffer(component.bufnr)
122 | elseif component.kind == "tab" then
123 | cx = tabs.get_tabpage(component.bufnr)
124 | elseif component.kind == "sidebar" then
125 | cx = { number = component.bufnr, side = component.sidebar }
126 | else
127 | cx = {}
128 | end
129 | if cx then
130 | cx.is_hovered = true
131 | end
132 | if component.on_mouse_enter then
133 | if
134 | (component.kind ~= "buffer" and component.kind ~= "tab") or cx ~= nil
135 | then
136 | component.on_mouse_enter(cx, current.screencol)
137 | end
138 | end
139 | M.set_hovered({
140 | index = component.index,
141 | bufnr = cx and cx.number,
142 | on_mouse_leave = component.on_mouse_leave,
143 | kind = component.kind,
144 | })
145 | end
146 |
147 | local function on_hover(current)
148 | M.clear_dragging()
149 | local hovered_component = M.hovered()
150 | if vim.o.showtabline == 0 then
151 | return
152 | end
153 | if current.screenrow == 1 then
154 | if
155 | last_position
156 | and hovered_component
157 | and last_position.screencol == current.screencol
158 | then
159 | return
160 | end
161 | local component = M.get_current(current.screencol)
162 |
163 | if
164 | component
165 | and hovered_component
166 | and component.index == hovered_component.index
167 | and component.bufnr == hovered_component.bufnr
168 | then
169 | return
170 | end
171 |
172 | if hovered_component ~= nil then
173 | mouse_leave(hovered_component)
174 | end
175 | if not component then
176 | vim.cmd.redrawtabline()
177 | return
178 | end
179 |
180 | mouse_enter(component, current)
181 | vim.cmd.redrawtabline()
182 | elseif hovered_component ~= nil then
183 | mouse_leave(hovered_component)
184 | vim.cmd.redrawtabline()
185 | end
186 | last_position = current
187 | end
188 |
189 | local function width(bufs, buf)
190 | return iter(bufs)
191 | :filter(function(v)
192 | return v.bufnr == buf
193 | end)
194 | :map(function(v)
195 | return v.width
196 | end)
197 | :fold(0, function(acc, v)
198 | return acc + v
199 | end)
200 | end
201 |
202 | local function start_pos(bufs, buf)
203 | local pos = 0
204 | for _, v in ipairs(bufs) do
205 | if v.bufnr == buf then
206 | return pos
207 | end
208 | pos = pos + v.width
209 | end
210 | return pos
211 | end
212 |
213 | local function on_drag(pos)
214 | local hovered_component = M.hovered()
215 | if hovered_component then
216 | local buf = buffers.get_buffer(hovered_component.bufnr)
217 | if buf then
218 | buf.is_hovered = false
219 | end
220 | if hovered_component.kind == "buffer" then
221 | if buf and hovered_component.on_mouse_leave then
222 | hovered_component.on_mouse_leave(buf)
223 | end
224 | elseif hovered_component.on_mouse_leave then
225 | hovered_component.on_mouse_leave()
226 | end
227 | M.clear_hovered()
228 | end
229 | if pos.screenrow ~= 1 then
230 | M.clear_dragging()
231 | return
232 | end
233 | if pos.dragging == "l" then
234 | local current, bufs = M.get_current(pos.screencol)
235 | if current == nil or bufs == nil or current.kind ~= "buffer" then
236 | return
237 | end
238 |
239 | -- if we're not dragging yet or we're dragging the same buffer, start dragging
240 | if M.dragging() == current.bufnr or not M.dragging() then
241 | M.set_dragging(current.bufnr)
242 | return
243 | end
244 |
245 | -- dragged buffer
246 | local dragged_buf = buffers.get_buffer(M.dragging())
247 | if not dragged_buf then
248 | return
249 | end
250 |
251 | -- current (hovered) buffer
252 | local cur_buf = buffers.get_buffer(current.bufnr)
253 | if not cur_buf then
254 | return
255 | end
256 |
257 | -- start position of dragged buffer
258 | local dragging_start = start_pos(bufs, M.dragging())
259 | -- width of the current (hovered) buffer
260 | local cur_buf_width = width(bufs, current.bufnr)
261 |
262 | if
263 | -- buffer is being dragged to the left
264 | (
265 | dragging_start > pos.screencol
266 | and pos.screencol + cur_buf_width > dragging_start
267 | )
268 | -- buffer is being dragged to the right
269 | or dragging_start + cur_buf_width <= pos.screencol
270 | then
271 | buffers.move_buffer(dragged_buf, cur_buf._valid_index)
272 | end
273 | end
274 | end
275 |
276 | function M.mouse_pos(drag, key)
277 | drag = drag or {}
278 | local ok, pos = pcall(vim.fn.getmousepos)
279 | if not ok then
280 | return
281 | end
282 |
283 | return {
284 | dragging = type(drag) == "table" and drag[key] or drag,
285 | screencol = pos.screencol,
286 | screenrow = pos.screenrow,
287 | line = pos.line,
288 | column = pos.column,
289 | winid = pos.winid,
290 | winrow = pos.winrow,
291 | wincol = pos.wincol,
292 | }
293 | end
294 |
295 | function M.setup()
296 | if config.mappings.disable_mouse == true or version.minor < 8 then
297 | return
298 | end
299 |
300 | if version.minor >= 9 and vim.on_key and vim.keycode then
301 | local mouse_move = vim.keycode("")
302 | local drag = {
303 | [vim.keycode("")] = "l",
304 | [vim.keycode("")] = "r",
305 | [vim.keycode("")] = "m",
306 | }
307 | vim.on_key(function(k)
308 | local data = M.mouse_pos(drag, k)
309 | if data then
310 | vim.schedule_wrap(function(key)
311 | if key == mouse_move then
312 | on_hover(data)
313 | elseif drag[key] then
314 | on_drag(data)
315 | end
316 | end)(k)
317 | end
318 | end)
319 | elseif vim.o.mousemoveevent then
320 | vim.keymap.set({ "n", "" }, "", function()
321 | local data = M.mouse_pos()
322 | if data then
323 | on_hover(data)
324 | end
325 | end)
326 | vim.keymap.set({ "n", "" }, "", function()
327 | local data = M.mouse_pos("l")
328 | if data then
329 | on_drag(data)
330 | end
331 | end)
332 | end
333 | end
334 |
335 | return M
336 |
--------------------------------------------------------------------------------
/lua/cokeline/init.lua:
--------------------------------------------------------------------------------
1 | local lazy = require("cokeline.lazy")
2 | local rendering = lazy("cokeline.rendering")
3 | local history = lazy("cokeline.history")
4 | local config = lazy("cokeline.config")
5 |
6 | local opt = vim.opt
7 |
8 | _G.cokeline = {}
9 |
10 | ---@param opts table|nil
11 | local setup = function(opts)
12 | config.setup(opts or {})
13 | if config.history and config.history.enabled then
14 | history.setup(config.history.size)
15 | end
16 |
17 | require("cokeline.mappings").setup()
18 | require("cokeline.hover").setup()
19 | require("cokeline.augroups").setup()
20 |
21 | opt.showtabline = 2
22 | opt.tabline = "%!v:lua.cokeline.tabline()"
23 | end
24 |
25 | ---@return string
26 | _G.cokeline.tabline = function()
27 | local visible_buffers = require("cokeline.buffers").get_visible()
28 | if #visible_buffers < config.show_if_buffers_are_at_least then
29 | opt.showtabline = 0
30 | return ""
31 | end
32 | return rendering.render(visible_buffers, config.fill_hl)
33 | end
34 |
35 | return {
36 | setup = setup,
37 | }
38 |
--------------------------------------------------------------------------------
/lua/cokeline/lazy.lua:
--------------------------------------------------------------------------------
1 | return function(module)
2 | return setmetatable({}, {
3 | __index = function(_, k)
4 | return require(module)[k]
5 | end,
6 | __newindex = function(_, k, v)
7 | require(module)[k] = v
8 | end,
9 | })
10 | end
11 |
--------------------------------------------------------------------------------
/lua/cokeline/mappings.lua:
--------------------------------------------------------------------------------
1 | local lazy = require("cokeline.lazy")
2 | local state = lazy("cokeline.state")
3 | local buffers = lazy("cokeline.buffers")
4 |
5 | local cmd = vim.cmd
6 | local filter = vim.tbl_filter
7 | local keymap = vim.keymap
8 | local set_keymap = vim.api.nvim_set_keymap
9 | local fn = vim.fn
10 |
11 | local is_picking = {
12 | focus = false,
13 | close = false,
14 | }
15 |
16 | ---@param goal '"switch"' | '"focus"' | '"close"' | fun(buf: Buffer)
17 | ---@param index index
18 | local by_index = function(goal, index)
19 | local target_buffer = filter(function(buffer)
20 | return buffer.index == index
21 | end, state.visible_buffers)[1]
22 |
23 | if not target_buffer then
24 | return
25 | end
26 |
27 | if goal == "switch" then
28 | local focused_buffer = filter(function(buffer)
29 | return buffer.is_focused
30 | end, state.valid_buffers)[1]
31 |
32 | if not focused_buffer then
33 | return
34 | end
35 |
36 | buffers.move_buffer(focused_buffer, target_buffer._valid_index)
37 | elseif goal == "focus" then
38 | target_buffer:focus()
39 | elseif goal == "close" then
40 | target_buffer:delete()
41 | elseif type(goal) == "function" then
42 | goal(target_buffer)
43 | end
44 | end
45 |
46 | ---@param goal '"switch"' | '"focus"' |'"close"' | fun(buf: Buffer, did_fallback: boolean)
47 | ---@param step -1 | 1
48 | local by_step = function(goal, step)
49 | local config = lazy("cokeline.config")
50 | local focused_buffer = filter(function(buffer)
51 | return buffer.is_focused
52 | end, state.valid_buffers)[1]
53 |
54 | local target_buf
55 | local target_idx
56 | local did_fallback = false
57 | if focused_buffer then
58 | target_idx = focused_buffer._valid_index + step
59 | if target_idx < 1 or target_idx > #state.valid_buffers then
60 | if not config.mappings.cycle_prev_next then
61 | return
62 | end
63 | target_idx = (target_idx - 1) % #state.valid_buffers + 1
64 | end
65 | target_buf = state.valid_buffers[target_idx]
66 | elseif goal == "focus" and config.history.enabled then
67 | did_fallback = true
68 | target_buf = require("cokeline.history"):last()
69 | if step < -1 then
70 | -- The result of history:last() is the index -1,
71 | -- so we need to step back one more.
72 | step = step + 1
73 | end
74 | if target_buf and step ~= 0 then
75 | local step_buf = state.valid_buffers[target_buf._valid_index + step]
76 | if step_buf then
77 | target_buf = step_buf
78 | end
79 | end
80 | else
81 | return
82 | end
83 |
84 | if target_buf then
85 | if goal == "switch" then
86 | buffers.move_buffer(focused_buffer, target_idx)
87 | elseif goal == "focus" then
88 | target_buf:focus()
89 | elseif goal == "close" and not did_fallback then
90 | target_buf:delete()
91 | elseif type(goal) == "function" then
92 | goal(target_buf, did_fallback)
93 | end
94 | end
95 | end
96 |
97 | ---@param goal '"focus"' | '"close"' | '"close-multiple"' | fun(buf: Buffer)
98 | local pick = function(goal)
99 | local close_multiple = false
100 | if goal == "close-multiple" then
101 | goal = "close"
102 | close_multiple = true
103 | end
104 |
105 | is_picking[goal] = true
106 | cmd("redrawtabline")
107 |
108 | repeat
109 | local valid_char, char = pcall(fn.getchar)
110 | if not close_multiple then
111 | is_picking[goal] = false
112 | end
113 |
114 | -- bail out on keyboard interrupt
115 | if not valid_char then
116 | char = 0
117 | end
118 |
119 | local letter = fn.nr2char(char)
120 | local target_buffer = filter(function(buffer)
121 | return buffer.pick_letter == letter
122 | end, state.visible_buffers)[1]
123 |
124 | if not target_buffer or (goal == "focus" and target_buffer.is_focused) then
125 | is_picking[goal] = false
126 | cmd("redrawtabline")
127 | return
128 | end
129 |
130 | if goal == "focus" then
131 | target_buffer:focus()
132 | elseif goal == "close" then
133 | target_buffer:delete()
134 | elseif type(goal) == "function" then
135 | goal(target_buffer)
136 | end
137 | until not is_picking[goal]
138 | end
139 |
140 | local setup = function()
141 | if keymap then
142 | keymap.set("n", "(cokeline-switch-prev)", function()
143 | by_step("switch", -1)
144 | end)
145 |
146 | keymap.set("n", "(cokeline-switch-next)", function()
147 | by_step("switch", 1)
148 | end)
149 |
150 | keymap.set("n", "(cokeline-focus-prev)", function()
151 | by_step("focus", -1)
152 | end)
153 |
154 | keymap.set("n", "(cokeline-focus-next)", function()
155 | by_step("focus", 1)
156 | end)
157 |
158 | keymap.set("n", "(cokeline-pick-focus)", function()
159 | pick("focus")
160 | end)
161 |
162 | keymap.set("n", "(cokeline-pick-close)", function()
163 | pick("close")
164 | end)
165 |
166 | keymap.set("n", "(cokeline-pick-close-multiple)", function()
167 | pick("close-multiple")
168 | end)
169 |
170 | for i = 1, 20 do
171 | keymap.set("n", ("(cokeline-switch-%s)"):format(i), function()
172 | by_index("switch", i)
173 | end, {})
174 |
175 | keymap.set("n", ("(cokeline-focus-%s)"):format(i), function()
176 | by_index("focus", i)
177 | end, {})
178 | end
179 | else
180 | set_keymap(
181 | "n",
182 | "(cokeline-switch-prev)",
183 | 'lua require"cokeline/mappings".by_step("switch", -1)',
184 | {}
185 | )
186 | set_keymap(
187 | "n",
188 | "(cokeline-switch-next)",
189 | 'lua require"cokeline/mappings".by_step("switch", 1)',
190 | {}
191 | )
192 | set_keymap(
193 | "n",
194 | "(cokeline-focus-prev)",
195 | 'lua require"cokeline/mappings".by_step("focus", -1)',
196 | {}
197 | )
198 | set_keymap(
199 | "n",
200 | "(cokeline-focus-next)",
201 | 'lua require"cokeline/mappings".by_step("focus", 1)',
202 | {}
203 | )
204 | set_keymap(
205 | "n",
206 | "(cokeline-pick-focus)",
207 | 'lua require"cokeline/mappings".pick("focus")',
208 | {}
209 | )
210 | set_keymap(
211 | "n",
212 | "(cokeline-pick-close)",
213 | 'lua require"cokeline/mappings".pick("close")',
214 | {}
215 | )
216 | set_keymap(
217 | "n",
218 | "(cokeline-pick-close-multiple)",
219 | 'lua require"cokeline/mappings".pick("close-multiple")',
220 | {}
221 | )
222 | for i = 1, 20 do
223 | set_keymap(
224 | "n",
225 | ("(cokeline-switch-%s)"):format(i),
226 | ('lua require"cokeline/mappings".by_index("switch", %s)'):format(
227 | i
228 | ),
229 | {}
230 | )
231 |
232 | set_keymap(
233 | "n",
234 | ("(cokeline-focus-%s)"):format(i),
235 | ('lua require"cokeline/mappings".by_index("focus", %s)'):format(
236 | i
237 | ),
238 | {}
239 | )
240 | end
241 | end
242 | end
243 |
244 | return {
245 | by_index = by_index,
246 | by_step = by_step,
247 | is_picking_focus = function()
248 | return is_picking.focus
249 | end,
250 | is_picking_close = function()
251 | return is_picking.close
252 | end,
253 | pick = pick,
254 | setup = setup,
255 | }
256 |
--------------------------------------------------------------------------------
/lua/cokeline/rendering.lua:
--------------------------------------------------------------------------------
1 | local lazy = require("cokeline.lazy")
2 | local state = lazy("cokeline.state")
3 | local config = lazy("cokeline.config")
4 | local components = lazy("cokeline.components")
5 | local sidebar = lazy("cokeline.sidebar")
6 | local rhs = lazy("cokeline.rhs")
7 | local tabs = lazy("cokeline.tabs")
8 | local RenderContext = lazy("cokeline.context")
9 | local iter = require("plenary.iterators").iter
10 |
11 | local insert = table.insert
12 | local sort = table.sort
13 | local unpack = unpack or table.unpack
14 |
15 | local extend = vim.list_extend
16 | local o = vim.o
17 |
18 | ---@type index
19 | local current_index
20 |
21 | ---@param buffers Buffer[]
22 | ---@param previous_buffer_index index
23 | ---@return Buffer
24 | local find_current_buffer = function(buffers, previous_buffer_index)
25 | local focused_buffer = iter(buffers):find(function(buffer)
26 | return buffer.is_focused
27 | end)
28 |
29 | return focused_buffer or buffers[previous_buffer_index] or buffers[#buffers]
30 | end
31 |
32 | ---@param c1 Component
33 | ---@param c2 Component
34 | ---@return boolean
35 | local by_decreasing_priority = function(c1, c2)
36 | return c1.truncation.priority < c2.truncation.priority
37 | end
38 |
39 | ---@param c1 Component
40 | ---@param c2 Component
41 | ---@return boolean
42 | local by_increasing_index = function(c1, c2)
43 | return c1.index < c2.index
44 | end
45 |
46 | -- Takes in either a single buffer or a list of buffers, and it returns a list
47 | -- of all the rendered components together with their total combined width.
48 | ---@param context Buffer|Buffer[]|TabPage|TabPage[]
49 | ---@param complist Component[]
50 | ---@return Component, number
51 | local function to_components(context, complist)
52 | local hovered = require("cokeline.hover").hovered()
53 | -- A simple heuristic to check if we're dealing with single buffer or a list
54 | -- of them is to just check if one of they keys is defined.
55 | if context.number then
56 | local cs = {}
57 | for _, c in ipairs(complist) do
58 | local render_cx
59 | if context.filetype then
60 | render_cx = RenderContext:buffer(context)
61 | render_cx.provider.buf_hovered = hovered ~= nil
62 | and hovered.bufnr == context.number
63 | else
64 | render_cx = RenderContext:tab(context)
65 | render_cx.provider.tab_hovered = hovered ~= nil
66 | and hovered.bufnr == context.number
67 | end
68 | render_cx.provider.is_hovered = hovered ~= nil
69 | and hovered.bufnr == context.number
70 | and hovered.index == c.index
71 | local rendered = c:render(render_cx)
72 | render_cx.provider.is_hovered = false
73 | if rendered.width > 0 then
74 | insert(cs, rendered)
75 | end
76 | end
77 |
78 | local width = components.width(cs)
79 | if width <= config.rendering.max_buffer_width then
80 | return cs, width
81 | else
82 | sort(cs, by_decreasing_priority)
83 | components.shorten(cs, config.rendering.max_buffer_width)
84 | sort(cs, by_increasing_index)
85 | return cs, config.rendering.max_buffer_width
86 | end
87 | else
88 | local cs = {}
89 | local width = 0
90 | for _, buffer in ipairs(context) do
91 | local buf_components, buf_width = to_components(buffer, complist)
92 | width = width + buf_width
93 | for _, component in pairs(buf_components) do
94 | insert(cs, component)
95 | end
96 | end
97 | return cs, width
98 | end
99 | end
100 |
101 | ---This is a helper function for rendering and hover handers. It takes
102 | ---the list of visible buffers, figures out which components to display, and
103 | ---returns a list of pre-render components
104 | ---@param visible_buffers Buffer[]
105 | ---@return table|string
106 | local prepare = function(visible_buffers)
107 | local sidebar_components_l = sidebar.get_components("left")
108 | local sidebar_components_r = sidebar.get_components("right")
109 | local rhs_components = rhs.get_components()
110 | local available_width = o.columns - components.width(sidebar_components_l)
111 | if available_width == 0 then
112 | return components.render(sidebar_components_l)
113 | end
114 |
115 | local tab_placement
116 | local tab_components
117 | local tabs_width
118 | if config.tabs then
119 | tab_placement = config.tabs.placement or "left"
120 | tab_components = to_components(tabs.get_tabs(), state.tabs)
121 | tabs_width = components.width(tab_components)
122 | available_width = available_width - tabs_width
123 | if available_width == 0 then
124 | return components.render(sidebar_components_l)
125 | .. components.render(tab_components)
126 | end
127 | end
128 |
129 | local current_buffer = find_current_buffer(visible_buffers, current_index)
130 |
131 | local current_components, current_width
132 | if current_buffer then
133 | current_index = current_buffer.index
134 |
135 | current_components, current_width =
136 | to_components(current_buffer, state.components)
137 | if current_width >= available_width then
138 | sort(current_components, by_decreasing_priority)
139 | components.shorten(current_components, available_width)
140 | sort(current_components, by_increasing_index)
141 | if current_buffer.index > 1 then
142 | components.shorten(current_components, available_width, "left")
143 | end
144 | if current_buffer.index < #visible_buffers then
145 | components.shorten(current_components, available_width, "right")
146 | end
147 |
148 | return {
149 | sidebar_left = sidebar_components_l,
150 | sidebar_right = sidebar_components_r,
151 | buffers = current_components,
152 | rhs = rhs_components,
153 | tabs = tab_components,
154 | gap = math.max(
155 | 0,
156 | available_width
157 | - (
158 | components.width(current_components)
159 | + components.width(rhs_components)
160 | )
161 | ),
162 | }
163 | end
164 | else
165 | current_components, current_width = {}, 0
166 | end
167 |
168 | local left_components, left_width
169 | local right_components, right_width
170 | if #visible_buffers > 0 then
171 | left_components, left_width = to_components({
172 | unpack(visible_buffers, 1, current_buffer.index - 1),
173 | }, state.components)
174 |
175 | right_components, right_width = to_components({
176 | unpack(visible_buffers, current_buffer.index + 1, #visible_buffers),
177 | }, state.components)
178 | else
179 | left_components, left_width = {}, 0
180 | right_components, right_width = {}, 0
181 | end
182 |
183 | local rhs_width = components.width(rhs_components)
184 | + components.width(sidebar_components_r)
185 | local available_width_left, available_width_right = config.rendering.slider(
186 | available_width
187 | - current_width
188 | - rhs_width
189 | - ((tabs_width ~= nil and tab_placement == "right") and tabs_width or 0),
190 | left_width,
191 | right_width
192 | )
193 |
194 | -- If we handled left, current and right components separately we might have
195 | -- to shorten the left or right components with a `to_width` parameter of 1,
196 | -- in which case the correct behaviour would be to "shorten" the current
197 | -- buffer components instead.
198 | --
199 | -- To avoid this, we join all the buffer components together *before*
200 | -- checking if they need to be shortened.
201 | local buffer_components =
202 | extend(extend(left_components, current_components), right_components)
203 |
204 | if left_width > available_width_left then
205 | components.shorten(
206 | buffer_components,
207 | available_width_left + current_width + right_width,
208 | "left"
209 | )
210 | end
211 | if right_width > available_width_right then
212 | components.shorten(buffer_components, available_width - rhs_width, "right")
213 | end
214 |
215 | local bufs_width = components.width(buffer_components)
216 | return {
217 | sidebar_left = sidebar_components_l,
218 | sidebar_right = sidebar_components_r,
219 | buffers = buffer_components,
220 | rhs = rhs_components,
221 | tabs = tab_components,
222 | gap = math.max(0, available_width - (bufs_width + rhs_width)),
223 | }
224 | end
225 |
226 | ---This is the main function responsible for rendering the bufferline. It takes
227 | ---the list of visible buffers, figures out which components to display and
228 | ---returns their rendered version.
229 | ---@param visible_buffers Buffer[]
230 | ---@param fill_hl string
231 | ---@return string
232 | local render = function(visible_buffers, fill_hl)
233 | local cx = prepare(visible_buffers)
234 | local rendered = components.render(cx.sidebar_left) .. "%#" .. fill_hl .. "#"
235 | if config.tabs and config.tabs.placement == "left" then
236 | rendered = rendered .. components.render(cx.tabs) .. "%#" .. fill_hl .. "#"
237 | end
238 | rendered = "%#"
239 | .. fill_hl
240 | .. "#"
241 | .. rendered
242 | .. components.render(cx.buffers)
243 | .. "%#"
244 | .. fill_hl
245 | .. "#"
246 | .. string.rep(" ", cx.gap)
247 | .. components.render(cx.rhs)
248 | if config.tabs and config.tabs.placement == "right" then
249 | rendered = rendered .. "%#" .. fill_hl .. "#" .. components.render(cx.tabs)
250 | end
251 | rendered = rendered .. components.render(cx.sidebar_right)
252 | return rendered
253 | end
254 |
255 | return {
256 | by_decreasing_priority = by_decreasing_priority,
257 | by_increasing_index = by_increasing_index,
258 | prepare = prepare,
259 | render = render,
260 | }
261 |
--------------------------------------------------------------------------------
/lua/cokeline/rhs.lua:
--------------------------------------------------------------------------------
1 | local lazy = require("cokeline.lazy")
2 | local state = lazy("cokeline.state")
3 | local components = lazy("cokeline.components")
4 | local RenderContext = lazy("cokeline.context")
5 |
6 | local M = {}
7 |
8 | ---@class RhsContext
9 | ---@field is_hovered boolean
10 |
11 | function M.get_components()
12 | local hovered = lazy("cokeline.hover").hovered()
13 | local rhs = {}
14 | for _, c in ipairs(state.rhs) do
15 | local is_hovered = hovered and hovered.index == c.index
16 | table.insert(
17 | rhs,
18 | c:render(RenderContext:rhs({
19 | is_hovered = is_hovered,
20 | }, "rhs"))
21 | )
22 | end
23 | return rhs
24 | end
25 |
26 | function M.width()
27 | return components.width(M.get_components())
28 | end
29 |
30 | return M
31 |
--------------------------------------------------------------------------------
/lua/cokeline/sidebar.lua:
--------------------------------------------------------------------------------
1 | local lazy = require("cokeline.lazy")
2 | local config = lazy("cokeline.config")
3 | local state = lazy("cokeline.state")
4 | local Buffer = lazy("cokeline.buffers").Buffer
5 | local components = lazy("cokeline.components")
6 | local RenderContext = lazy("cokeline.context")
7 |
8 | local min = math.min
9 | local rep = string.rep
10 | local insert = table.insert
11 | local sort = table.sort
12 |
13 | local api = vim.api
14 | local bo = vim.bo
15 | local fn = vim.fn
16 | local o = vim.o
17 |
18 | local width_cache = {}
19 |
20 | local get_win = function(side)
21 | local layout = fn.winlayout()
22 | if not layout then
23 | return
24 | end
25 |
26 | -- If the first split level is not given by vertically split windows we
27 | -- return early and invalidate the width cache.
28 | if layout[1] ~= "row" then
29 | width_cache = {}
30 | return nil
31 | end
32 |
33 | -- The second element of the `layout` table is a nested list representing the
34 | -- tree of vertically split windows in a tabpage. For example, for a layout
35 | -- like.
36 | -- +-----+-----+-----+
37 | -- | | | C |
38 | -- | A | B |-----|
39 | -- | | | D |
40 | -- +-----+-----+-----+
41 | -- the associated tree would be
42 | -- / | \
43 | -- / | \
44 | -- A B / \
45 | -- C D
46 | -- where each leaf is represented as a `{'leaf', }` table.
47 | local window_tree = layout[2]
48 |
49 | -- Since we're checking if we need to display sidebars we're only
50 | -- interested in the first and last window splits.
51 | --
52 | -- However, with edgy.nvim, a sidebar can contain multiple splits nested one level in.
53 | -- So if the first window found is a leaf, we check the first window of the container.
54 | local first_split
55 | local winid
56 | if side == nil or side == "left" then
57 | first_split = window_tree[1]
58 | winid = first_split[2]
59 | if first_split[1] ~= "leaf" then
60 | local win = first_split[2][1]
61 | if win and win[1] == "leaf" then
62 | winid = win[2]
63 | else
64 | width_cache[side] = nil
65 | return nil
66 | end
67 | end
68 | else
69 | first_split = window_tree[#window_tree]
70 | winid = first_split[2]
71 | if first_split[1] ~= "leaf" then
72 | local win = first_split[2][#first_split[2]]
73 | if win and win[1] == "leaf" then
74 | winid = win[2]
75 | else
76 | width_cache[side] = nil
77 | return nil
78 | end
79 | end
80 | end
81 | width_cache[side] = api.nvim_win_get_width(winid)
82 | return winid
83 | end
84 |
85 | ---@param side "left" | "right"
86 | ---@return Component[]
87 | local get_components = function(side)
88 | if not config.sidebar then
89 | return {}
90 | end
91 |
92 | local winid = get_win(side)
93 | if not winid then
94 | return {}
95 | end
96 |
97 | local bufnr = api.nvim_win_get_buf(winid)
98 |
99 | if type(config.sidebar.filetype) == "table" then
100 | if not vim.tbl_contains(config.sidebar.filetype, bo[bufnr].filetype) then
101 | return {}
102 | end
103 | else
104 | if bo[bufnr].filetype ~= config.sidebar.filetype then
105 | return {}
106 | end
107 | end
108 |
109 | local buffer = Buffer.new({
110 | bufnr = bufnr,
111 | name = fn.bufname(4),
112 | })
113 | local sidebar_width = min(api.nvim_win_get_width(winid), o.columns)
114 |
115 | local sidebar_components = {}
116 | local width = 0
117 | local id = #state.components + #state.rhs + 1
118 | local hover = lazy("cokeline.hover").hovered()
119 | buffer.buf_hovered = hover ~= nil and hover.bufnr == buffer.number
120 | if not state.sidebar or not next(state.sidebar) then
121 | -- Fix(170): Return early as we have no sidebar components to handle
122 | return sidebar_components
123 | end
124 | for _, c in ipairs(state.sidebar) do
125 | c.sidebar = side
126 | buffer.is_hovered = hover ~= nil
127 | and hover.index == id
128 | and hover.bufnr == buffer.number
129 | local component = c:render(RenderContext:buffer(buffer))
130 | buffer.is_hovered = false
131 | -- We need at least one component, otherwise we can't add padding to the
132 | -- last component if needed.
133 | if component.width > 0 or #sidebar_components == 0 then
134 | insert(sidebar_components, component)
135 | width = width + component.width
136 | end
137 | id = id + 1
138 | end
139 |
140 | local rendering = lazy("cokeline.rendering")
141 |
142 | if width > sidebar_width then
143 | sort(sidebar_components, rendering.by_decreasing_priority)
144 | components.shorten(sidebar_components, sidebar_width)
145 | sort(sidebar_components, rendering.by_increasing_index)
146 | elseif width < sidebar_width then
147 | local space_left = sidebar_width - width
148 | local last = #sidebar_components
149 | sidebar_components[last].text = sidebar_components[last].text
150 | .. rep(" ", space_left)
151 | sidebar_components[last].width = sidebar_components[last].width
152 | + space_left
153 | end
154 |
155 | return sidebar_components
156 | end
157 |
158 | return {
159 | get_win = get_win,
160 | get_components = get_components,
161 | ---@return integer
162 | get_width = function(side)
163 | return width_cache[side] or 0
164 | end,
165 | }
166 |
--------------------------------------------------------------------------------
/lua/cokeline/sliders.lua:
--------------------------------------------------------------------------------
1 | -- local math_abs = math.abs
2 | local floor = math.floor
3 | local max = math.max
4 |
5 | ---@type table
6 | -- local gl_mut_prev_state
7 |
8 | -- local gl_scrolloff = 5
9 |
10 | ---@param available_space number
11 | ---@param width_left_of_current number
12 | ---@param width_right_of_current number
13 | ---@return number, number
14 | local center_current_buffer = function(
15 | available_space,
16 | width_left_of_current,
17 | width_right_of_current
18 | )
19 | local available_space_left = floor(available_space / 2)
20 | local available_space_right = available_space_left + available_space % 2
21 |
22 | local unused_space_left =
23 | max(available_space_left - width_left_of_current, 0)
24 | local unused_space_right =
25 | max(available_space_right - width_right_of_current, 0)
26 |
27 | return available_space_left - unused_space_left + unused_space_right,
28 | available_space_right - unused_space_right + unused_space_left
29 | end
30 |
31 | ---@param available_space number
32 | ---@param width_left_of_current number
33 | ---@param width_right_of_current number
34 | ---@return number, number
35 | -- local slide_if_needed =
36 | -- function(available_space, width_left_of_current, width_right_of_current,
37 | -- current_buffer_index, prev_state)
38 |
39 | -- -- The first time the bufferline is rendered there is no previous state, so..
40 | -- if not gl_mut_prev_state then
41 | -- return 0, available_space
42 | -- end
43 |
44 | -- -- If the new current buffer was not fully in view in the previous state we..
45 | -- if current_buffer_index < prev_state.fully_in_view[1].index then
46 | -- local left =
47 | -- gl_scrolloff < width_left_of_current
48 | -- and gl_scrolloff
49 | -- or width_left_of_current
50 | -- local right = available_space - left
51 | -- return left, right
52 |
53 | -- elseif current_buffer_index >
54 | -- prev_state.fully_in_view[#prev_state.fully_in_view].index then
55 | -- local right =
56 | -- gl_scrolloff < width_right_of_current
57 | -- and gl_scrolloff
58 | -- or width_right_of_current
59 | -- local left = available_space - right
60 | -- return left, right
61 | -- end
62 |
63 | -- local index_range = math_abs(current_buffer_index - prev_state.current_index)
64 | -- local j = prev_state.current_index < current_buffer_index and 1 or -1
65 |
66 | -- local left, right = prev_state.spaces.left, prev_state.spaces.right
67 | -- for i=1,index_range do
68 | -- left = left + j * prev_state.fully_in_view[i].width
69 | -- right = right - j * prev_state.fully_in_view[i + 1].width
70 | -- end
71 |
72 | -- return left, right
73 | -- end
74 |
75 | return {
76 | center_current_buffer = center_current_buffer,
77 | -- slide_if_needed = slide_if_needed,
78 | }
79 |
--------------------------------------------------------------------------------
/lua/cokeline/state.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 |
3 | M.tab_cache = {}
4 |
5 | M.tab_lookup = {}
6 |
7 | M.valid_lookup = {}
8 |
9 | M.visible_buffers = {}
10 |
11 | M.valid_buffers = {}
12 |
13 | ---@type Component[]
14 | M.components = {}
15 |
16 | ---@type Component[]
17 | M.rhs = {}
18 |
19 | ---@type Component[]
20 | M.sidebar = {}
21 |
22 | ---@type Component[]
23 | M.tabs = {}
24 |
25 | return M
26 |
--------------------------------------------------------------------------------
/lua/cokeline/tabs.lua:
--------------------------------------------------------------------------------
1 | local lazy = require("cokeline.lazy")
2 | local state = lazy("cokeline.state")
3 | local buffers = lazy("cokeline.buffers")
4 | local Buffer = buffers.Buffer
5 |
6 | local M = {}
7 |
8 | ---@class Window
9 | ---@field number window
10 | ---@field buffer Buffer
11 | local Window = {}
12 | Window.__index = Window
13 |
14 | function Window.new(winnr)
15 | local bufnr = vim.api.nvim_win_get_buf(winnr)
16 | local win = {
17 | number = winnr,
18 | buffer = buffers.get_buffer(bufnr)
19 | or Buffer.new({ bufnr = bufnr, name = vim.api.nvim_buf_get_name(bufnr) }),
20 | }
21 | return setmetatable(win, Window)
22 | end
23 |
24 | function Window:close()
25 | pcall(vim.api.nvim_win_close, self.number, false)
26 | end
27 |
28 | function Window:focus()
29 | vim.api.nvim_set_current_win(self.number)
30 | end
31 |
32 | ---@class TabPage
33 | ---@field number tabpage
34 | ---@field index number
35 | ---@field windows Window[]
36 | ---@field focused Window
37 | ---@field is_first boolean
38 | ---@field is_last boolean
39 | ---@field is_active boolean
40 | ---@field is_hovered boolean Whether the current component is hovered
41 | ---@field tab_hovered boolean Whether the tab is hovered
42 | local TabPage = {}
43 | TabPage.__index = TabPage
44 |
45 | function TabPage.new(tabnr, index, is_first, is_last, is_active)
46 | local active_win = vim.api.nvim_tabpage_get_win(tabnr)
47 | local windows = vim.api.nvim_tabpage_list_wins(tabnr)
48 |
49 | local focused
50 | for i, winnr in ipairs(windows) do
51 | windows[i] = Window.new(winnr)
52 | if winnr == active_win then
53 | focused = windows[i]
54 | end
55 | end
56 |
57 | local tab = {
58 | number = tabnr,
59 | index = index,
60 | windows = windows,
61 | focused = focused,
62 | is_active = is_active,
63 | is_first = is_first,
64 | is_last = is_last,
65 | }
66 | return setmetatable(tab, TabPage)
67 | end
68 |
69 | function TabPage:focus()
70 | vim.api.nvim_set_current_tabpage(self.number)
71 | end
72 |
73 | function TabPage:close()
74 | for _, win in ipairs(self.windows) do
75 | win:close()
76 | end
77 | end
78 |
79 | function M.update_current(tabnr)
80 | for _, t in ipairs(state.tab_cache) do
81 | if t.number == tabnr then
82 | t.is_active = true
83 | else
84 | t.is_active = false
85 | end
86 | end
87 | end
88 |
89 | function M.fetch_tabs()
90 | local tabs = {}
91 | local tabnrs = vim.api.nvim_list_tabpages()
92 | local active_tab = vim.api.nvim_get_current_tabpage()
93 | table.sort(tabnrs, function(a, b)
94 | return a < b
95 | end)
96 | for _, tabnr in ipairs(tabnrs) do
97 | local t = vim.api.nvim_tabpage_get_number(tabnr)
98 | if state.tab_lookup[tabnr] ~= nil then
99 | tabs[t] = state.tab_lookup[tabnr]
100 | tabs[t].is_active = tabnr == active_tab
101 | tabs[t].is_first = t == 1
102 | tabs[t].is_last = t == #tabnrs
103 | tabs[t].index = t
104 | local windows = vim.api.nvim_tabpage_list_wins(tabnr)
105 | for i, winnr in ipairs(windows) do
106 | if
107 | tabs[t].windows[i] == nil
108 | or tabs[t].windows[i].buffer == nil
109 | or tabs[t].windows[i].buffer.number
110 | ~= vim.api.nvim_win_get_buf(winnr)
111 | then
112 | tabs[t].windows[i] = Window.new(winnr)
113 | end
114 | end
115 | else
116 | tabs[t] =
117 | TabPage.new(tabnr, t, t == 1, t == #tabnrs, tabnr == active_tab)
118 | state.tab_lookup[tabnr] = tabs[t]
119 | end
120 | end
121 | state.tab_cache = tabs
122 | end
123 |
124 | function M.get_tabs()
125 | local cache_count = state.tab_cache and #state.tab_cache or nil
126 | if
127 | cache_count == nil
128 | or cache_count == 0
129 | or cache_count ~= #vim.api.nvim_list_tabpages()
130 | then
131 | M.fetch_tabs()
132 | end
133 | return state.tab_cache
134 | end
135 |
136 | function M.get_tabpage(tabnr)
137 | return state.tab_lookup[tabnr]
138 | end
139 |
140 | return M
141 |
--------------------------------------------------------------------------------
/lua/cokeline/utils.lua:
--------------------------------------------------------------------------------
1 | ---@param bufnr number
2 | ---@param focus_next boolean
3 | ---@param wipeout boolean
4 | ---@param force boolean
5 | local function buf_del_impl(bufnr, focus_next, wipeout, force)
6 | local win = vim.fn.bufwinid(bufnr)
7 |
8 | if win ~= -1 then
9 | -- Get a list of buffers that are valid switch targets
10 | local switchable = vim.tbl_filter(function(buf)
11 | return vim.api.nvim_buf_is_valid(buf)
12 | and vim.bo[buf].buflisted
13 | and buf ~= bufnr
14 | end, vim.api.nvim_list_bufs())
15 |
16 | local switch_target
17 | if #switchable > 0 then
18 | for _, switch_nr in ipairs(switchable) do
19 | -- If we're looking for a buffer after the current one, break here
20 | if switch_nr < bufnr then
21 | -- Keep looping to find the previous buffer
22 | -- This also serves as a fallback if there's no
23 | -- next buffer, and `focus_next` is true
24 | switch_target = switch_nr
25 | end
26 | if switch_nr > bufnr and (focus_next or switch_target == nil) then
27 | -- We found the next buffer, break
28 | switch_target = switch_nr
29 | break
30 | end
31 | end
32 | else
33 | -- If there's no possible switch target, create a new buffer and switch to it
34 | switch_target = vim.api.nvim_create_buf(false, true)
35 | if switch_target == 0 then
36 | vim.api.nvim_err_writeln("Failed to create new buffer")
37 | end
38 | end
39 |
40 | vim.api.nvim_win_set_buf(win, switch_target)
41 | end
42 |
43 | if vim.api.nvim_buf_is_valid(bufnr) then
44 | if wipeout then
45 | vim.cmd.bwipeout({ count = bufnr })
46 | else
47 | vim.api.nvim_buf_delete(bufnr, { force = force })
48 | end
49 | end
50 | end
51 |
52 | ---@param bufnr bufnr The buffer to delete
53 | ---@param focus "prev" | "next" | nil Buffer to focus on deletion (default: "next")
54 | ---@param wipeout boolean Whether to wipe the buffer (default: false)
55 | ---Deletes a buffer but keeps window layout
56 | local function buf_delete(bufnr, focus, wipeout)
57 | bufnr = bufnr or 0
58 | wipeout = wipeout or false
59 | local focus_next = true
60 |
61 | if focus == "prev" then
62 | focus_next = false
63 | end
64 |
65 | if vim.bo[bufnr].modified then
66 | vim.ui.select({
67 | "Save and close",
68 | "Discard changes and close",
69 | "Cancel",
70 | }, {
71 | prompt = string.format(
72 | "Buffer %s has unsaved changes.",
73 | vim.fn.fnamemodify(vim.fn.bufname(bufnr), ":f")
74 | ),
75 | }, function(_, choice)
76 | if choice == 1 then
77 | if vim.api.nvim_buf_get_name(bufnr) == "" then
78 | vim.ui.input({
79 | prompt = "File name: ",
80 | completion = "file",
81 | }, function(name)
82 | if name and name ~= "" then
83 | vim.api.nvim_buf_set_name(bufnr, name)
84 | vim.api.nvim_buf_call(bufnr, vim.cmd.write)
85 | buf_del_impl(bufnr, focus_next, wipeout, false)
86 | end
87 | end)
88 | else
89 | vim.api.nvim_buf_call(bufnr, vim.cmd.write)
90 | buf_del_impl(bufnr, focus_next, wipeout, false)
91 | end
92 | elseif choice == 2 then
93 | buf_del_impl(bufnr, focus_next, wipeout, true)
94 | elseif choice == 3 then
95 | return
96 | end
97 | end)
98 | elseif vim.bo[bufnr].buftype == "terminal" then
99 | vim.ui.select({
100 | "Quit",
101 | "Cancel",
102 | }, {
103 | prompt = string.format(
104 | "Buffer %s is a terminal, and is still running.",
105 | vim.fn.fnamemodify(vim.fn.bufname(bufnr), ":t")
106 | ),
107 | }, function(_, choice)
108 | if choice == 1 then
109 | buf_del_impl(bufnr, focus_next, wipeout, true)
110 | elseif choice == 2 then
111 | return
112 | end
113 | end)
114 | else
115 | buf_del_impl(bufnr, focus_next, wipeout, false)
116 | end
117 | end
118 |
119 | return {
120 | buf_delete = buf_delete,
121 | }
122 |
--------------------------------------------------------------------------------
/lua/resession/extensions/cokeline.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 |
3 | function M.on_save()
4 | local files = {}
5 | for entry in require("cokeline.history"):iter() do
6 | table.insert(files, entry.path)
7 | end
8 | return files
9 | end
10 |
11 | function M.on_post_load(data)
12 | local history = require("cokeline.history")
13 | for _, path in ipairs(data) do
14 | local buf = vim.fn.bufnr(path)
15 | if buf then
16 | history:push(buf)
17 | end
18 | end
19 | end
20 |
21 | return M
22 |
--------------------------------------------------------------------------------
/neovim.yml:
--------------------------------------------------------------------------------
1 | ---
2 | base: lua51
3 |
4 | globals:
5 | vim:
6 | any: true
7 | assert:
8 | args:
9 | - type: bool
10 | - type: string
11 | required: false
12 | after_each:
13 | args:
14 | - type: function
15 | before_each:
16 | args:
17 | - type: function
18 | describe:
19 | args:
20 | - type: string
21 | - type: function
22 | it:
23 | args:
24 | - type: string
25 | - type: function
26 |
--------------------------------------------------------------------------------
/selene.toml:
--------------------------------------------------------------------------------
1 | std = "neovim"
2 |
3 | [rules]
4 | global_usage = "allow"
5 | multiple_statements = "allow"
6 |
--------------------------------------------------------------------------------
/stylua.toml:
--------------------------------------------------------------------------------
1 | column_width = 79
2 | indent_type = "Spaces"
3 | indent_width = 2
4 |
--------------------------------------------------------------------------------