Full Code of GustavoKatel/sidebar.nvim for AI

main 082e4903c165 cached
39 files
154.7 KB
37.6k tokens
1 requests
Download .txt
Repository: GustavoKatel/sidebar.nvim
Branch: main
Commit: 082e4903c165
Files: 39
Total size: 154.7 KB

Directory structure:
gitextract_r5xv7lik/

├── .editorconfig
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       ├── docgen.yaml
│       └── lint.yaml
├── .gitignore
├── .luacheckrc
├── README.md
├── doc/
│   ├── general.md
│   └── sidebar.txt
├── lua/
│   ├── sidebar-nvim/
│   │   ├── bindings.lua
│   │   ├── builtin/
│   │   │   ├── buffers.lua
│   │   │   ├── containers.lua
│   │   │   ├── datetime.lua
│   │   │   ├── diagnostics.lua
│   │   │   ├── files.lua
│   │   │   ├── git-status.lua
│   │   │   ├── git.lua
│   │   │   ├── lsp-diagnostics.lua
│   │   │   ├── symbols.lua
│   │   │   └── todos.lua
│   │   ├── colors.lua
│   │   ├── components/
│   │   │   ├── basic.lua
│   │   │   └── loclist.lua
│   │   ├── config.lua
│   │   ├── debouncer.lua
│   │   ├── docker_utils.lua
│   │   ├── events.lua
│   │   ├── lib.lua
│   │   ├── profile.lua
│   │   ├── renderer.lua
│   │   ├── updater.lua
│   │   ├── utils.lua
│   │   └── view.lua
│   └── sidebar-nvim.lua
├── plugin/
│   └── sidebar-nvim.vim
├── selene.toml
├── stylua.toml
└── vim.toml

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

================================================
FILE: .editorconfig
================================================
root = true

[*]
insert_final_newline = true
end_of_line = lf

[*.lua]
indent_style = space
indent_size = 2


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Environment (please complete the following information):**
 - nvim version
 - sidebar setup
 - dotfiles (if any)

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea
title: ''
labels: enhancement
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/workflows/docgen.yaml
================================================
name: Generate Documentation

on:
  workflow_dispatch:
  push:
    branches:
      - dev
      - feat/docs

jobs:
  docs:
    runs-on: ubuntu-latest
    name: pandoc to vimdoc
    steps:
      - uses: actions/checkout@v2
      - name: panvimdoc
        uses: kdheepak/panvimdoc@main
        with:
          vimdoc: sidebar
          # Show Last Change on header
          description: ""
          # Input file
          pandoc: "doc/general.md"
          # Show Table of Content
          toc: true
          version: "NVIM v0.6.0"
      - uses: peter-evans/create-pull-request@v3
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          author: sidebar-nvim-bot <sidebar-nvim-bot@users.noreply.github.com>
          commit-message: "docs: autogenerate"
          branch: "docs/auto-update"
          delete-branch: true
          base: "dev"
          title: "[docs]: Update documentation"
          body: |
            Automated changes by the [docgen workflow](https://github.com/sidebar-nvim/sidebar.nvim/actions/workflows/docgen.yaml)


================================================
FILE: .github/workflows/lint.yaml
================================================
name: Linting & Formatting

on:
  push:
    branches: [main, dev]
  pull_request:

concurrency: 
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: leafo/gh-actions-lua@master
        with:
          luaVersion: "luajit-2.1.0-beta3"

      - uses: leafo/gh-actions-luarocks@v4.0.0

      - name: Install linter
        run: luarocks install luacheck

      - name: Lint
        run: luacheck lua

      - uses: JohnnyMorganz/stylua-action@v3
        with:
         token: ${{ secrets.GITHUB_TOKEN }}
         version: v0.20.0 # NOTE: we recommend pinning to a specific version in case of formatting changes
         # CLI arguments
         args: --check lua


================================================
FILE: .gitignore
================================================
.DS_Store

doc/tags


================================================
FILE: .luacheckrc
================================================
-- vim: ft=lua tw=80

-- Don't report unused self arguments of methods.
self = false

ignore = {
  "631",  -- max_line_length
}

-- Global objects defined by the C code
globals = {
  "vim",
}


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

A generic and modular lua sidebar inspired by [lualine](https://github.com/hoob3rt/lualine.nvim)

Development status: Alpha - bugs are expected

![screenshot](./demo/screenshot.png)

## Quick start

```lua
local sidebar = require("sidebar-nvim")
local opts = {open = true}
sidebar.setup(opts)
```

See [options](./doc/general.md#options) for a full list of setup options

## Requirements

- Neovim 0.6
- Nerdfonts (requirement by the default icons, you can change the icons to remove this requirement)

## Quick links

- [Options, commands, api and colors](./doc/general.md)
- [Builtin sections](./doc/general.md#builtin-sections)
- [Custom sections](./doc/general.md#custom-sections)


## TODOs (Need help)

- [ ] Allow sections to define custom hl groups for icons
- [ ] See repo issues, any contribution is really appreciated
- [x] Better section icons - users can define custom icons see [Options](./doc/general.md#options)
- [x] Improve docs + write vim help files - thanks @aspeddro


## Third party sections

- [dap-sidebar.nvim](https://github.com/GustavoKatel/dap-sidebar.nvim) - Show Dap breakpoints in the sidebar

## Development

We are trying to limit the frequency of merges to the default branch, therefore less noise during plugin update and possible less bugs.

If you don't mind frequent updates and/or want to test new features, you might want to check the `dev` branch.

Before reporting a bug, please check if the bug still exists in the `dev` branch.

## References

We based most of the code from the awesome work of @kyazdani42 in [nvim-tree](https://github.com/kyazdani42/nvim-tree.lua)

## Contributors

[@GustavoKatel](https://github.com/GustavoKatel/)
[@davysson](https://github.com/davysson/)


================================================
FILE: doc/general.md
================================================
# Overview

A generic and modular lua sidebar inspired by lualine

# Installing

You can install sidebar using any package manager.

With [packer.nvim](https://github.com/wbthomason/packer.nvim)

```lua
use 'sidebar-nvim/sidebar.nvim'
```

# Setup

Minimal setup:

```lua
require("sidebar-nvim").setup()
```

# Options

The following code block shows the defaults options:

```lua
require("sidebar-nvim").setup({
    disable_default_keybindings = 0,
    bindings = nil,
    open = false,
    side = "left",
    initial_width = 35,
    hide_statusline = false,
    update_interval = 1000,
    sections = { "datetime", "git", "diagnostics" },
    section_separator = {"", "-----", ""},
    section_title_separator = {""},
    containers = {
        attach_shell = "/bin/sh", show_all = true, interval = 5000,
    },
    datetime = { format = "%a %b %d, %H:%M", clocks = { { name = "local" } } },
    todos = { ignored_paths = { "~" } },
})
```

- `disable_default_keybindings` (number): Enable/disable the default keybindings. Default is `0`

- `bindings` (function): Attach custom bindings to the sidebar buffer. Default is `nil`

Example:

```lua
require("sidebar-nvim").setup({
    bindings = { ["q"] = function() require("sidebar-nvim").close() end }
})
```

Note sections can override these bindings, please see [Section Bindings](#section-bindings)

- `side` (string): Side of sidebar. Default is `'left'`

- `initial_width` (number): Width of sidebar. Default is `50`

- `hide_statusline` (bool): Show or hide sidebar statusline. Default is `false`

- `update_interval` (number): Update frequency in milliseconds. Default is `1000`

- `sections` (table): Which sections should the sidebar render. Default is `{ "datetime", "git", "diagnostics" }`

See [Builtin Sections](#builtin-sections) and [Custom Sections](#custom-sections)

- `section_separator` (string | table | function): Section separator mark, can be a string, a table or a function. Default is `{"", "-----", ""}`

    ```lua
    -- Using a function
    -- It needs to return a table
    function section_separator(section, index)
        return { "-----" }
    end
    ```

  `section` is the section definition. See [Custom Sections](#custom-sections) for more info

  `index` count from the `sections` table

- `section_title_separator` (string | table | function): Section title separator mark. This is rendered between the section title and the section content. It can be a string, a table or a function. Default is `{""}`

    ```lua
    -- Using a function
    -- It needs to return a table
    function section_title_separator(section, index)
        return { "-----" }
    end
    ```

  `section` is the section definition. See [Custom Sections](#custom-sections) for more info

  `index` count from the `sections` table

# Lua API

Public Lua api is available as: `require("sidebar-nvim").<function>`

- `toggle()`: Open/close the view

- `close()`: Close if open, otherwise no-op

- `open()`: Open if closed, otherwise no-op

- `update()`: Immediately update the view and the sections

- `resize(size)`: Resize the view width to `size`

  Parameters:

    - `size` (number): Resize the view width

- `focus()`: Move the cursor to the sidebar window

- `get_width(tabpage)`: Get the current width of the view from the current `tabpage`.

  Parameters:

    - `tabpage` (number): is the tab page number, if null it will return the width in the current tab page

- `reset_highlight()`: Use in case of errors. Clear the current highlighting so it can be re-rendered

# Commands

- `SidebarNvimToggle`: Open/Close the view
- `SidebarNvimClose`: Close if open, otherwise no-op
- `SidebarNvimOpen`: Open if closed, otherwise no-op
- `SidebarNvimUpdate`: Immediately update the view and the sections
- `SidebarNvimResize size`: Resize the view width to size, `size` is a number
- `SidebarNvimFocus`: Move the cursor to the sidebar window


# Custom Sections

sidebar.nvim accepts user defined sections. The minimal section definition is a table with a `draw` function that returns the string ready to render in the sidebar and a title. See below the list of available properties

```lua
local section = {
    title = "Section Title",
    icon = "->",
    setup = function(ctx)
        -- called only once and if the section is being used
    end,
    update = function(ctx)
        -- hook callback, called when an update was requested by either the user of external events (using autocommands)
    end,
    draw = function(ctx)
        return "> string here\n> multiline"
    end,
    highlights = {
        groups = { MyHighlightGroup = { gui="#C792EA", fg="#ff0000", bg="#00ff00" } },
        links = { MyHighlightGroupLink = "Keyword" }
    }
}

```

## section.icon

String with the icon or a function that returns a string

```lua
local section = {
    icon = function()
        return "#"
    end,
    -- or
    -- icon = "#"
}
```

## section.setup

This function is called only once *and* only if the section is being used
You can use this function to create timers, background jobs etc

## section.update

This plugin can request the section to update its internal state by calling this function. You may use this to avoid calling expensive functions during draw.

NOTE: This does not have any debouncing and it may be called multiples times, you may want to use a [debouncer](#debouncer)

Events that trigger section updates:

- `BufWritePost *`
- `VimResume *`
- `FocusGained *`

## section.draw

The function accepts a single parameter `ctx` containing the current width of the sidebar:

```lua
{ width = 90 }
```

The draw function may appear in three forms:

- Returning a string
- Returning a table of strings
- Returning a table like `{ lines = "", hl = {} }`

The later is used to specify the highlight groups related to the lines returned

Example:

```lua

local section = {
    title = "test",
    draw = function()
        return {
            lines = {"> item1", "> item2"},
            hl = {
                -- { <group name>, <line index relative to the returned lines>, <column start>, <column end, -1 means end of the line> }
                { "SectionMarker", 0, 0, 1 },
            }
        }
    end
}

```

## section.highlights

Specify the highlight groups associated with this section. This table contains two properties: `groups` and `links`

- `groups` define new highlight groups
- `links` link highlight groups to another

Example:

```lua
local section = {
    title = "Custom Section",
    icon = "->",
    draw = function()
        return {
            lines = {"hello world"},
            hl = {
                -- more info see `:h nvim_buf_add_highlight()`
                { "CustomHighlightGroupHello", 0, 0, 5 }, -- adds `CustomHighlightGroupHello` to the word "hello"
                { "CustomHighlightGroupWorld", 0, 6, -1 }, -- adds `CustomHighlightGroupWorld` to the word "world"
            },
        }
    end,
    highlights = {
        groups = { CustomHighlightGroupHello = { gui="#ff0000", fg="#00ff00", bg="#0000ff" } },
        links = { CustomHighlightGroupWorld = "Keyword" }
    }
}
```

more info see: [:h nvim_buf_add_highlight](https://neovim.io/doc/user/api.html#nvim_buf_add_highlight())

## section.bindings

Custom sections can define custom bindings. Bindings are dispatched to the section that the cursor is currently over.

This means that multiple sections can define the same bindings and SidebarNvim will dispatch to the correct section depending on the cursor position.

Example:

```lua
local lines = {"hello", "world"}
local section = {
    title = "Custom Section",
    icon = "->",
    draw = function()
        return lines
    end,
    bindings = {
        ["e"] = function(line, col)
            print("current word: "..lines[line])
        end,
    },
}
```

# Builtin components

Builtin components abstract ui elements that can be reused within sections.

## Loclist

Create a location list with collapsable groups.

Sections using it: [git](#git), [diagnostics](#diagnostics) and [todos](#todos)

Example:
```lua
local Loclist = require("sidebar-nvim.components.loclist")
local loclist = Loclist:new({
    group_icon = { closed = "", opened = "" },
    -- badge showing the number of items in each group
    show_group_count = true,
    -- if empty groups should be displayed
    show_empty_groups = true,
    -- if there's a single group, skip rendering the group controls
    omit_single_group = false,
    -- initial state of the groups
    groups_initially_closed = false,
    -- highlight groups for each control element
    highlights = {
        group = "SidebarNvimLabel",
        group_count = "SidebarNvimSectionTitle",
    },
})

loclist:add_item({
    group = "my_group",
    lnum = 1,
    col = 2,
    left = {
        { text = "text on the left", hl = "MyHighlightGroup" }
    },
    right = {
        { text = "text on the right", hl = "MyHighlightGroup" }
    },
    order = 1
})

-- inside the section draw function
local lines, hl = {}, {}

table.insert(lines, "Here's the location list you asked:")

loclist:draw(ctx, lines, hl)

return { lines = lines, hl = hl }

```

### loclist:add_item {doc=sidebar-loclist-add-item}

adds a new item to the loclist. Example: `loclist:add_item(item)`

Parameters:

- `item.group` (string) the group name that this item will live in
- `item.lnum` (number) the line number of this item
- `item.col` (number) the col number of this item
- `item.left` (table) the text that should be shown on the left of the item in the format:

`item.left = { { text = "my", hl = "MyHighlightGroup" }, { text = " text", hl = "MyHighlightGroup2" } }`

This will result in `my text` in the section with the first word with highlight group `MyHighlightGroup` and the second with `MyHighlightGroup2`

- `item.right` (table) same as `item.left` but shown on the right side
- `item.order` (number) all items are sorted before drawn on the screen, use this to define each item priority

### loclist:set_items {doc=sidebar-loclist-set-items}

this method receive a list of items and call [loclist:add_item](###loclist-add_item) to each one of them

optionally users can pass a second parameter `clear_opts` (table) which is passed to [loclist:clear](###loclist-clear) before adding new items

### loclist:clear {doc=sidebar-loclist-clear}

remove all items

Parameters:

- `clear_opts` (table)
- `clear_opts.remove_groups` (boolean) if true, also remove groups from the list, otherwise only items will be removed, removing groups from the list also means that the state of groups will be cleared

# Utils

## Debouncer

This can be used to avoid multiple calls within a certain time frame. It's useful if you want to avoid multiple expensive computations in sequence.

Example:

```lua
local Debouncer = require("sidebar-nvim.debouncer")

local function expensive_computation(n)
    print(n + 1)
end

local expensive_computation_debounced = Debouncer:new(expensive_computation, 1000)

expensive_computation_debounced:call(42) -- print(43)
expensive_computation_debounced:call(42) -- does nothing

vim.defer_fn(function()
    expensive_computation_debounced:call(43) -- print(44)
    expensive_computation_debounced:call(43) -- does nothing
end, 1500)
```

# Builtin Sections

## datetime

Prints the current date and time using. You can define multiple clocks with different timezones or offsets.

NOTE: In order to use timezones you need to install `luatz` from luarocks, like the following if using `packer`:
```lua
use {
    "sidebar-nvim/sidebar.nvim",
    rocks = {'luatz'}
}
```

This dependency is optional, you can use the `offset` parameter to change the clock, which does not require extra dependencies.

### config

Example configuration:

```lua
require("sidebar-nvim").setup({
    ...
    datetime = {
        icon = "",
        format = "%a %b %d, %H:%M",
        clocks = {
            { name = "local" }
        }
    }
    ...
})
```

Clock options:
```lua
{
    name = "clock name", -- defaults to `tz`
    tz = "America/Los_Angeles", -- only works if using `luatz`, defaults to current timezone
    offset = -8, -- this is ignored if tz is present, defaults to 0
}
```

You can see a list of all [available timezones here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)

## git

Prints the status of the repo as returned by `git status --porcelain`

### config

Example configuration:

```lua
require("sidebar-nvim").setup({
    ...
    ["git"] = {
        icon = "",
    }
    ...
})
```

### keybindings

| key | when | action |
|-----|------|--------|
| `e` | hovering filename | open file in the previous window
| `s` | hovering filename | stage files
| `u` | hovering filename | unstage files

## diagnostics

Prints the current status of the builtin lsp grouper by file. It shows only loaded buffers

### config

```lua
require("sidebar-nvim").setup({
    ...
    ["diagnostics"] = {
        icon = "",
    }
    ...
})
```


### keybindings

| key | when | action |
|-----|------|--------|
| `e` | hovering diagnostic message | open file in the previous window at the diagnostic position
| `t` | hovering filename | toggle collapse on the group

## todos

Shows the TODOs in source. Provided by RipGrep.

### config

```lua
require("sidebar-nvim").setup({
    ...
    todos = {
        icon = "",
        ignored_paths = {'~'}, -- ignore certain paths, this will prevent huge folders like $HOME to hog Neovim with TODO searching
        initially_closed = false, -- whether the groups should be initially closed on start. You can manually open/close groups later.
    }
    ...
})
```

### keybindings

| key | when | action |
|-----|------|--------|
| `e` | hovering todo location | open file in the previous window at the todo position
| `t` | hovering the group | toggle collapse on the group

### functions

The following functions are available to the user to control this specific section elements.

<!-- panvimdoc renders subheading-4 differnt, so use heading-5 here instead -->

##### toggle_all()

Toggle all groups, i.e.: NOTE, TODO, FIXME etc.

Call like the following: `require("sidebar-nvim.builtin.todos").<function>`

##### close_all()

Close all groups.

##### open_all()

Open all groups.

##### open(group_name)

Opens the group with name `group_name`. Example `require("sidebar-nvim.builtin.todos").open("NOTE")`

##### close(group_name)

Closes the group with name `group_name`. Example `require("sidebar-nvim.builtin.todos").close("NOTE")`

##### toggle(group_name)

Toggle the group with name `group_name`. Example `require("sidebar-nvim.builtin.todos").toggle("NOTE")`

## containers

Shows the system docker containers. Collected from `docker ps -a '--format=\'{"Names": {{json .Names}}, "State": {{json .State}}, "ID": {{json .ID}} }\''`

NOTE: in some environments this can be a very intensive command to run. You may see increased cpu usage when this section is enabled.

### config

```lua
require("sidebar-nvim").setup({
    ...
    containers = {
        icon = "",
        use_podman = false,
        attach_shell = "/bin/sh",
        show_all = true, -- whether to run `docker ps` or `docker ps -a`
        interval = 5000, -- the debouncer time frame to limit requests to the docker daemon
    }
    ...
})
```

### keybindings

| key | when | action |
|-----|------|--------|
| `e` | hovering a container location | open a new terminal and attach to the container with `docker exec -it <container id> ${config.containers.attach_shell}`

## buffers

Shows current loaded buffers.


### config

```lua
require("sidebar-nvim").setup({
    ...
    buffers = {
        icon = "",
        ignored_buffers = {}, -- ignore buffers by regex
        sorting = "id", -- alternatively set it to "name" to sort by buffer name instead of buf id
        show_numbers = true, -- whether to also show the buffer numbers
        ignore_not_loaded = false, -- whether to ignore not loaded buffers
        ignore_terminal = true, -- whether to show terminal buffers in the list
    }
    ...
})
```

### keybindings

| key | when | action |
|-----|------|--------|
| `d` | hovering an item | close the identified buffer
| `e` | hovering an item | open the identified buffer in a window
| `w` | hovering an item | save the identified buffer


## files

Shows/manage current directory structure.


### config

```lua
require("sidebar-nvim").setup({
    ...
    files = {
        icon = "",
        show_hidden = false,
        ignored_paths = {"%.git$"}
    }
    ...
})
```

### keybindings

| key | when | action |
|-----|------|--------|
| `d` | hovering an item | delete file/folder
| `y` | hovering an item | yank/copy a file/folder
| `x` | hovering an item | cut a file/folder
| `p` | hovering an item | paste a file/folder
| `c` | hovering an item | create a new file
| `e` | hovering an item | open the current file/folder
| `r` | hovering an item | rename file/folder
| `u` | hovering the section | undo operation
| `<C-r>` | hovering the section | redo operation
| `<CR>` | hovering an item | open file/folder


## symbols

Shows lsp symbols for the current buffer.


### config

```lua
require("sidebar-nvim").setup({
    ...
    symbols = {
        icon = "ƒ",
    }
    ...
})
```

### keybindings

| key | when | action |
|-----|------|--------|
| `t` | hovering an item | toggle group
| `e` | hovering an item | open location

# Colors

| Highlight Group | Defaults To |
| --------------- | ----------- |
| *SidebarNvimSectionTitle* | Directory |
| *SidebarNvimSectionSeparator* | Comment |
| *SidebarNvimSectionTitleSeparator* | Comment |
| *SidebarNvimNormal* | Normal |
| *SidebarNvimLabel* | Label |
| *SidebarNvimComment* | Comment |
| *SidebarNvimLineNr* | LineNr |
| *SidebarNvimKeyword* | Keyword |
| *SidebarNvimGitStatusState* | SidebarNvimKeyword |
| *SidebarNvimGitStatusFileName* | SidebarNvimNormal |
| *SidebarNvimLspDiagnosticsError* | LspDiagnosticsDefaultError |
| *SidebarNvimLspDiagnosticsWarn* | LspDiagnosticsDefaultWarning |
| *SidebarNvimLspDiagnosticsInfo* | LspDiagnosticsDefaultInformation |
| *SidebarNvimLspDiagnosticsHint* | LspDiagnosticsDefaultHint |
| *SidebarNvimLspDiagnosticsLineNumber* | SidebarNvimLineNr |
| *SidebarNvimLspDiagnosticsColNumber* | SidebarNvimLineNr |
| *SidebarNvimLspDiagnosticsFilename* | SidebarNvimLabel |
| *SidebarNvimLspDiagnosticsTotalNumber* | LspTroubleCount |
| *SidebarNvimLspDiagnosticsMessage* | SidebarNvimNormal |
| *SidebarNvimTodoTag* | SidebarNvimLabel |
| *SidebarNvimTodoTotalNumber* | SidebarNvimNormal |
| *SidebarNvimTodoFilename* | SidebarNvimNormal |
| *SidebarNvimTodoLineNumber* | SidebarNvimLineNr |
| *SidebarNvimTodoColNumber* | SidebarNvimLineNr |
| *SidebarNvimDockerContainerStatusRunning* | LspDiagnosticsDefaultInformation |
| *SidebarNvimDockerContainerStatusExited* | LspDiagnosticsDefaultError |
| *SidebarNvimDockerContainerName* | SidebarNvimNormal |
| *SidebarNvimDatetimeClockName* | SidebarNvimComment |
| *SidebarNvimDatetimeClockValue* | SidebarNvimNormal |
| *SidebarNvimBuffersActive* | SidebarNvimSectionTitle |
| *SidebarNvimBuffersNumber* | SidebarNvimComment |


================================================
FILE: doc/sidebar.txt
================================================
*sidebar.txt*             For NVIM v0.6.0            Last change: 2022 June 21

==============================================================================
Table of Contents                                  *sidebar-table-of-contents*

1. Overview                                                 |sidebar-overview|
2. Installing                                             |sidebar-installing|
3. Setup                                                       |sidebar-setup|
4. Options                                                   |sidebar-options|
5. Lua API                                                   |sidebar-lua-api|
6. Commands                                                 |sidebar-commands|
7. Custom Sections                                   |sidebar-custom-sections|
  - section.icon                                        |sidebar-section.icon|
  - section.setup                                      |sidebar-section.setup|
  - section.update                                    |sidebar-section.update|
  - section.draw                                        |sidebar-section.draw|
  - section.highlights                            |sidebar-section.highlights|
  - section.bindings                                |sidebar-section.bindings|
8. Builtin components                             |sidebar-builtin-components|
  - Loclist                                                  |sidebar-loclist|
9. Utils                                                       |sidebar-utils|
  - Debouncer                                              |sidebar-debouncer|
10. Builtin Sections                                |sidebar-builtin-sections|
  - datetime                                                |sidebar-datetime|
  - git                                                          |sidebar-git|
  - diagnostics                                          |sidebar-diagnostics|
  - todos                                                      |sidebar-todos|
  - containers                                            |sidebar-containers|
  - buffers                                                  |sidebar-buffers|
  - files                                                      |sidebar-files|
  - symbols                                                  |sidebar-symbols|
11. Colors                                                    |sidebar-colors|

==============================================================================
1. Overview                                                 *sidebar-overview*

A generic and modular lua sidebar inspired by lualine

==============================================================================
2. Installing                                             *sidebar-installing*

You can install sidebar using any package manager.

With packer.nvim <https://github.com/wbthomason/packer.nvim>

>
    use 'sidebar-nvim/sidebar.nvim'
<


==============================================================================
3. Setup                                                       *sidebar-setup*

Minimal setup:

>
    require("sidebar-nvim").setup()
<


==============================================================================
4. Options                                                   *sidebar-options*

The following code block shows the defaults options:

>
    require("sidebar-nvim").setup({
        disable_default_keybindings = 0,
        bindings = nil,
        open = false,
        side = "left",
        initial_width = 35,
        hide_statusline = false,
        update_interval = 1000,
        sections = { "datetime", "git", "diagnostics" },
        section_separator = {"", "-----", ""},
        section_title_separator = {""},
        containers = {
            attach_shell = "/bin/sh", show_all = true, interval = 5000,
        },
        datetime = { format = "%a %b %d, %H:%M", clocks = { { name = "local" } } },
        todos = { ignored_paths = { "~" } },
    })
<



- `disable_default_keybindings` (number): Enable/disable the default keybindings.
    Default is `0`
- `bindings` (function): Attach custom bindings to the sidebar buffer. Default is
    `nil`


Example:

>
    require("sidebar-nvim").setup({
        bindings = { ["q"] = function() require("sidebar-nvim").close() end }
    })
<


Note sections can override these bindings, please see
|sidebar-section-bindings|


- `side` (string): Side of sidebar. Default is `'left'`
- `initial_width` (number): Width of sidebar. Default is `50`
- `hide_statusline` (bool): Show or hide sidebar statusline. Default is `false`
- `update_interval` (number): Update frequency in milliseconds. Default is `1000`
- `sections` (table): Which sections should the sidebar render. Default is `{
    "datetime", "git", "diagnostics" }`


See |sidebar-builtin-sections| and |sidebar-custom-sections|


- `section_separator` (string | table | function): Section separator mark, can be
    a string, a table or a function. Default is `{"", "-----", ""}`
    >
        -- Using a function
        -- It needs to return a table
        function section_separator(section, index)
            return { "-----" }
        end
    <
    `section` is the section definition. See |sidebar-custom-sections| for more
    info
    `index` count from the `sections` table
- `section_title_separator` (string | table | function): Section title separator
    mark. This is rendered between the section title and the section content. It
    can be a string, a table or a function. Default is `{""}`
    >
        -- Using a function
        -- It needs to return a table
        function section_title_separator(section, index)
            return { "-----" }
        end
    <
    `section` is the section definition. See |sidebar-custom-sections| for more
    info
    `index` count from the `sections` table


==============================================================================
5. Lua API                                                   *sidebar-lua-api*

Public Lua api is available as: `require("sidebar-nvim").<function>`


- `toggle()`: Open/close the view
- `close()`: Close if open, otherwise no-op
- `open()`: Open if closed, otherwise no-op
- `update()`: Immediately update the view and the sections
- `resize(size)`: Resize the view width to `size`
    Parameters:
    - `size` (number): Resize the view width
- `focus()`: Move the cursor to the sidebar window
- `get_width(tabpage)`: Get the current width of the view from the current
    `tabpage`.
    Parameters:
    - `tabpage` (number): is the tab page number, if null it will return the width in the current tab page
- `reset_highlight()`: Use in case of errors. Clear the current highlighting so
    it can be re-rendered


==============================================================================
6. Commands                                                 *sidebar-commands*


- `SidebarNvimToggle`: Open/Close the view
- `SidebarNvimClose`: Close if open, otherwise no-op
- `SidebarNvimOpen`: Open if closed, otherwise no-op
- `SidebarNvimUpdate`: Immediately update the view and the sections
- `SidebarNvimResize size`: Resize the view width to size, `size` is a number
- `SidebarNvimFocus`: Move the cursor to the sidebar window


==============================================================================
7. Custom Sections                                   *sidebar-custom-sections*

sidebar.nvim accepts user defined sections. The minimal section definition is a
table with a `draw` function that returns the string ready to render in the
sidebar and a title. See below the list of available properties

>
    local section = {
        title = "Section Title",
        icon = "->",
        setup = function(ctx)
            -- called only once and if the section is being used
        end,
        update = function(ctx)
            -- hook callback, called when an update was requested by either the user of external events (using autocommands)
        end,
        draw = function(ctx)
            return "> string here\n> multiline"
        end,
        highlights = {
            groups = { MyHighlightGroup = { gui="#C792EA", fg="#ff0000", bg="#00ff00" } },
            links = { MyHighlightGroupLink = "Keyword" }
        }
    }
<


SECTION.ICON                                            *sidebar-section.icon*

String with the icon or a function that returns a string

>
    local section = {
        icon = function()
            return "#"
        end,
        -- or
        -- icon = "#"
    }
<


SECTION.SETUP                                          *sidebar-section.setup*

This function is called only once _and_ only if the section is being used You
can use this function to create timers, background jobs etc

SECTION.UPDATE                                        *sidebar-section.update*

This plugin can request the section to update its internal state by calling
this function. You may use this to avoid calling expensive functions during
draw.

NOTE: This does not have any debouncing and it may be called multiples times,
you may want to use a |sidebar-debouncer|

Events that trigger section updates:


- `BufWritePost *`
- `VimResume *`
- `FocusGained *`


SECTION.DRAW                                            *sidebar-section.draw*

The function accepts a single parameter `ctx` containing the current width of
the sidebar:

>
    { width = 90 }
<


The draw function may appear in three forms:


- Returning a string
- Returning a table of strings
- Returning a table like `{ lines = "", hl = {} }`


The later is used to specify the highlight groups related to the lines returned

Example:

>
    
    local section = {
        title = "test",
        draw = function()
            return {
                lines = {"> item1", "> item2"},
                hl = {
                    -- { <group name>, <line index relative to the returned lines>, <column start>, <column end, -1 means end of the line> }
                    { "SectionMarker", 0, 0, 1 },
                }
            }
        end
    }
<


SECTION.HIGHLIGHTS                                *sidebar-section.highlights*

Specify the highlight groups associated with this section. This table contains
two properties: `groups` and `links`


- `groups` define new highlight groups
- `links` link highlight groups to another


Example:

>
    local section = {
        title = "Custom Section",
        icon = "->",
        draw = function()
            return {
                lines = {"hello world"},
                hl = {
                    -- more info see `:h nvim_buf_add_highlight()`
                    { "CustomHighlightGroupHello", 0, 0, 5 }, -- adds `CustomHighlightGroupHello` to the word "hello"
                    { "CustomHighlightGroupWorld", 0, 6, -1 }, -- adds `CustomHighlightGroupWorld` to the word "world"
                },
            }
        end,
        highlights = {
            groups = { CustomHighlightGroupHello = { gui="#ff0000", fg="#00ff00", bg="#0000ff" } },
            links = { CustomHighlightGroupWorld = "Keyword" }
        }
    }
<


more info see: |:h nvim_buf_add_highlight|

SECTION.BINDINGS                                    *sidebar-section.bindings*

Custom sections can define custom bindings. Bindings are dispatched to the
section that the cursor is currently over.

This means that multiple sections can define the same bindings and SidebarNvim
will dispatch to the correct section depending on the cursor position.

Example:

>
    local lines = {"hello", "world"}
    local section = {
        title = "Custom Section",
        icon = "->",
        draw = function()
            return lines
        end,
        bindings = {
            ["e"] = function(line, col)
                print("current word: "..lines[line])
            end,
        },
    }
<


==============================================================================
8. Builtin components                             *sidebar-builtin-components*

Builtin components abstract ui elements that can be reused within sections.

LOCLIST                                                      *sidebar-loclist*

Create a location list with collapsable groups.

Sections using it: |sidebar-git|, |sidebar-diagnostics| and |sidebar-todos|

Example:

>
    local Loclist = require("sidebar-nvim.components.loclist")
    local loclist = Loclist:new({
        group_icon = { closed = "", opened = "" },
        -- badge showing the number of items in each group
        show_group_count = true,
        -- if empty groups should be displayed
        show_empty_groups = true,
        -- if there's a single group, skip rendering the group controls
        omit_single_group = false,
        -- initial state of the groups
        groups_initially_closed = false,
        -- highlight groups for each control element
        highlights = {
            group = "SidebarNvimLabel",
            group_count = "SidebarNvimSectionTitle",
        },
    })
    
    loclist:add_item({
        group = "my_group",
        lnum = 1,
        col = 2,
        left = {
            { text = "text on the left", hl = "MyHighlightGroup" }
        },
        right = {
            { text = "text on the right", hl = "MyHighlightGroup" }
        },
        order = 1
    })
    
    -- inside the section draw function
    local lines, hl = {}, {}
    
    table.insert(lines, "Here's the location list you asked:")
    
    loclist:draw(ctx, lines, hl)
    
    return { lines = lines, hl = hl }
<


LOCLIST:ADD_ITEM ~

adds a new item to the loclist. Example: `loclist:add_item(item)`

Parameters:


- `item.group` (string) the group name that this item will live in
- `item.lnum` (number) the line number of this item
- `item.col` (number) the col number of this item
- `item.left` (table) the text that should be shown on the left of the item in the format:


`item.left = { { text = "my", hl = "MyHighlightGroup" }, { text = " text", hl =
"MyHighlightGroup2" } }`

This will result in `my text` in the section with the first word with highlight
group `MyHighlightGroup` and the second with `MyHighlightGroup2`


- `item.right` (table) same as `item.left` but shown on the right side
- `item.order` (number) all items are sorted before drawn on the screen, use this to define each item priority


LOCLIST:SET_ITEMS ~

this method receive a list of items and call |sidebar-loclist:add_item| to each
one of them

optionally users can pass a second parameter `clear_opts` (table) which is
passed to |sidebar-loclist:clear| before adding new items

LOCLIST:CLEAR ~

remove all items

Parameters:


- `clear_opts` (table)
- `clear_opts.remove_groups` (boolean) if true, also remove groups from the list, otherwise only items will be removed, removing groups from the list also means that the state of groups will be cleared


==============================================================================
9. Utils                                                       *sidebar-utils*

DEBOUNCER                                                  *sidebar-debouncer*

This can be used to avoid multiple calls within a certain time frame. It’s
useful if you want to avoid multiple expensive computations in sequence.

Example:

>
    local Debouncer = require("sidebar-nvim.debouncer")
    
    local function expensive_computation(n)
        print(n + 1)
    end
    
    local expensive_computation_debounced = Debouncer:new(expensive_computation, 1000)
    
    expensive_computation_debounced:call(42) -- print(43)
    expensive_computation_debounced:call(42) -- does nothing
    
    vim.defer_fn(function()
        expensive_computation_debounced:call(43) -- print(44)
        expensive_computation_debounced:call(43) -- does nothing
    end, 1500)
<


==============================================================================
10. Builtin Sections                                *sidebar-builtin-sections*

DATETIME                                                    *sidebar-datetime*

Prints the current date and time using. You can define multiple clocks with
different timezones or offsets.

NOTE: In order to use timezones you need to install `luatz` from luarocks, like
the following if using `packer`:

>
    use {
        "sidebar-nvim/sidebar.nvim",
        rocks = {'luatz'}
    }
<


This dependency is optional, you can use the `offset` parameter to change the
clock, which does not require extra dependencies.

CONFIG ~

Example configuration:

>
    require("sidebar-nvim").setup({
        ...
        datetime = {
            icon = "",
            format = "%a %b %d, %H:%M",
            clocks = {
                { name = "local" }
            }
        }
        ...
    })
<


Clock options:

>
    {
        name = "clock name", -- defaults to `tz`
        tz = "America/Los_Angeles", -- only works if using `luatz`, defaults to current timezone
        offset = -8, -- this is ignored if tz is present, defaults to 0
    }
<


You can see a list of all available timezones here
<https://en.wikipedia.org/wiki/List_of_tz_database_time_zones>

GIT                                                              *sidebar-git*

Prints the status of the repo as returned by `git status --porcelain`

CONFIG ~

Example configuration:

>
    require("sidebar-nvim").setup({
        ...
        ["git"] = {
            icon = "",
        }
        ...
    })
<


KEYBINDINGS ~

│ key │      when       │             action             │
│e    │hovering filename│open file in the previous window│
│s    │hovering filename│stage files                     │
│u    │hovering filename│unstage files                   │


DIAGNOSTICS                                              *sidebar-diagnostics*

Prints the current status of the builtin lsp grouper by file. It shows only
loaded buffers

CONFIG ~

>
    require("sidebar-nvim").setup({
        ...
        ["diagnostics"] = {
            icon = "",
        }
        ...
    })
<


KEYBINDINGS ~

│ key │           when            │                          action                           │
│e    │hovering diagnostic message│open file in the previous window at the diagnostic position│
│t    │hovering filename          │toggle collapse on the group                               │


TODOS                                                          *sidebar-todos*

Shows the TODOs in source. Provided by RipGrep.

CONFIG ~

>
    require("sidebar-nvim").setup({
        ...
        todos = {
            icon = "",
            ignored_paths = {'~'}, -- ignore certain paths, this will prevent huge folders like $HOME to hog Neovim with TODO searching
            initially_closed = false, -- whether the groups should be initially closed on start. You can manually open/close groups later.
        }
        ...
    })
<


KEYBINDINGS ~

│ key │         when         │                       action                        │
│e    │hovering todo location│open file in the previous window at the todo position│
│t    │hovering the group    │toggle collapse on the group                         │


FUNCTIONS ~

The following functions are available to the user to control this specific
section elements.

TOGGLE_ALL()

Toggle all groups, i.e.: NOTE, TODO, FIXME etc.

Call like the following: `require("sidebar-nvim.builtin.todos").<function>`

CLOSE_ALL()

Close all groups.

OPEN_ALL()

Open all groups.

OPEN(GROUP_NAME)

Opens the group with name `group_name`. Example
`require("sidebar-nvim.builtin.todos").open("NOTE")`

CLOSE(GROUP_NAME)

Closes the group with name `group_name`. Example
`require("sidebar-nvim.builtin.todos").close("NOTE")`

TOGGLE(GROUP_NAME)

Toggle the group with name `group_name`. Example
`require("sidebar-nvim.builtin.todos").toggle("NOTE")`

CONTAINERS                                                *sidebar-containers*

Shows the system docker containers. Collected from `docker ps -a
'--format=\'{"Names": {{json .Names}}, "State": {{json .State}}, "ID": {{json
.ID}} }\''`

NOTE: in some environments this can be a very intensive command to run. You may
see increased cpu usage when this section is enabled.

CONFIG ~

>
    require("sidebar-nvim").setup({
        ...
        containers = {
            icon = "",
            use_podman = false,
            attach_shell = "/bin/sh",
            show_all = true, -- whether to run `docker ps` or `docker ps -a`
            interval = 5000, -- the debouncer time frame to limit requests to the docker daemon
        }
        ...
    })
<


KEYBINDINGS ~

│ key │            when             │                                                         action                                                          │
│e    │hovering a container location│open a new terminal and attach to the container with docker exec -it <container id> ${config.containers.attach_shell}    │


BUFFERS                                                      *sidebar-buffers*

Shows current loaded buffers.

CONFIG ~

>
    require("sidebar-nvim").setup({
        ...
        buffers = {
            icon = "",
            ignored_buffers = {}, -- ignore buffers by regex
            sorting = "id", -- alternatively set it to "name" to sort by buffer name instead of buf id
            show_numbers = true, -- whether to also show the buffer numbers
            ignore_not_loaded = false, -- whether to ignore not loaded buffers
            ignore_terminal = true, -- whether to show terminal buffers in the list
        }
        ...
    })
<


KEYBINDINGS ~

│ key │      when      │                action                │
│d    │hovering an item│close the identified buffer           │
│e    │hovering an item│open the identified buffer in a window│
│w    │hovering an item│save the identified buffer            │


FILES                                                          *sidebar-files*

Shows/manage current directory structure.

CONFIG ~

>
    require("sidebar-nvim").setup({
        ...
        files = {
            icon = "",
            show_hidden = false,
            ignored_paths = {"%.git$"}
        }
        ...
    })
<


KEYBINDINGS ~

│   key   │        when        │           action           │
│d        │hovering an item    │delete file/folder          │
│y        │hovering an item    │yank/copy a file/folder     │
│x        │hovering an item    │cut a file/folder           │
│p        │hovering an item    │paste a file/folder         │
│c        │hovering an item    │create a new file           │
│e        │hovering an item    │open the current file/folder│
│r        │hovering an item    │rename file/folder          │
│u        │hovering the section│undo operation              │
│<C-r>    │hovering the section│redo operation              │
│<CR>     │hovering an item    │open file/folder            │


SYMBOLS                                                      *sidebar-symbols*

Shows lsp symbols for the current buffer.

CONFIG ~

>
    require("sidebar-nvim").setup({
        ...
        symbols = {
            icon = "ƒ",
        }
        ...
    })
<


KEYBINDINGS ~

│ key │      when      │   action    │
│t    │hovering an item│toggle group │
│e    │hovering an item│open location│


==============================================================================
11. Colors                                                    *sidebar-colors*

│             Highlight Group             │          Defaults To           │
│_SidebarNvimSectionTitle_                │Directory                       │
│_SidebarNvimSectionSeparator_            │Comment                         │
│_SidebarNvimSectionTitleSeparator_       │Comment                         │
│_SidebarNvimNormal_                      │Normal                          │
│_SidebarNvimLabel_                       │Label                           │
│_SidebarNvimComment_                     │Comment                         │
│_SidebarNvimLineNr_                      │LineNr                          │
│_SidebarNvimKeyword_                     │Keyword                         │
│_SidebarNvimGitStatusState_              │SidebarNvimKeyword              │
│_SidebarNvimGitStatusFileName_           │SidebarNvimNormal               │
│_SidebarNvimLspDiagnosticsError_         │LspDiagnosticsDefaultError      │
│_SidebarNvimLspDiagnosticsWarn_          │LspDiagnosticsDefaultWarning    │
│_SidebarNvimLspDiagnosticsInfo_          │LspDiagnosticsDefaultInformation│
│_SidebarNvimLspDiagnosticsHint_          │LspDiagnosticsDefaultHint       │
│_SidebarNvimLspDiagnosticsLineNumber_    │SidebarNvimLineNr               │
│_SidebarNvimLspDiagnosticsColNumber_     │SidebarNvimLineNr               │
│_SidebarNvimLspDiagnosticsFilename_      │SidebarNvimLabel                │
│_SidebarNvimLspDiagnosticsTotalNumber_   │LspTroubleCount                 │
│_SidebarNvimLspDiagnosticsMessage_       │SidebarNvimNormal               │
│_SidebarNvimTodoTag_                     │SidebarNvimLabel                │
│_SidebarNvimTodoTotalNumber_             │SidebarNvimNormal               │
│_SidebarNvimTodoFilename_                │SidebarNvimNormal               │
│_SidebarNvimTodoLineNumber_              │SidebarNvimLineNr               │
│_SidebarNvimTodoColNumber_               │SidebarNvimLineNr               │
│_SidebarNvimDockerContainerStatusRunning_│LspDiagnosticsDefaultInformation│
│_SidebarNvimDockerContainerStatusExited_ │LspDiagnosticsDefaultError      │
│_SidebarNvimDockerContainerName_         │SidebarNvimNormal               │
│_SidebarNvimDatetimeClockName_           │SidebarNvimComment              │
│_SidebarNvimDatetimeClockValue_          │SidebarNvimNormal               │
│_SidebarNvimBuffersActive_               │SidebarNvimSectionTitle         │
│_SidebarNvimBuffersNumber_               │SidebarNvimComment              │


Generated by panvimdoc <https://github.com/kdheepak/panvimdoc>

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


================================================
FILE: lua/sidebar-nvim/bindings.lua
================================================
local api = vim.api
local utils = require("sidebar-nvim.utils")

local config = require("sidebar-nvim.config")

local M = {}

M.State = {
    -- bindings defined by the sections
    -- map(index -> key string)
    section_bindings = {},
    -- fallback bindings if none of the sections have overrided them
    view_bindings = {
        ["q"] = function()
            require("sidebar-nvim").close()
        end,
    },
}

function M.setup()
    local user_mappings = config.bindings or {}
    if config.disable_default_keybindings == 1 then
        M.State.view_bindings = user_mappings
    else
        local result = vim.tbl_extend("force", M.State.view_bindings, user_mappings)
        M.State.view_bindings = result
    end

    for section_index, section_data in ipairs(config.sections) do
        local section = utils.resolve_section(section_index, section_data)
        if section and section.bindings ~= nil then
            M.update_section_bindings(section_index, section.bindings)
        end
    end
end

function M.update_section_bindings(index, bindings)
    for key, binding in pairs(bindings) do
        M.State.section_bindings[key] = M.State.section_bindings[key] or {}
        M.State.section_bindings[key][index] = binding
    end
end

function M.inject(bufnr)
    for direction, keys in pairs({ down = { "<Down>", "j" }, up = { "<Up>", "k" } }) do
        for _, key in ipairs(keys) do
            api.nvim_buf_set_keymap(
                bufnr,
                "n",
                key,
                utils.sidebar_nvim_cursor_move_callback(direction),
                { noremap = true, silent = true, nowait = true }
            )
        end
    end

    for key, _ in pairs(M.State.view_bindings) do
        api.nvim_buf_set_keymap(
            bufnr,
            "n",
            key,
            utils.sidebar_nvim_callback(key),
            { noremap = true, silent = true, nowait = true }
        )
    end

    for key, _ in pairs(M.State.section_bindings) do
        api.nvim_buf_set_keymap(
            bufnr,
            "n",
            key,
            utils.sidebar_nvim_callback(key),
            { noremap = true, silent = true, nowait = true }
        )
    end
end

local function execute_binding(key, binding, ...)
    if type(binding) ~= "function" then
        utils.echo_warning("binding for '" .. key .. "' expected to be a function")
        return
    end
    binding(...)
end

-- @return boolean
-- @return whether the binding was defined or not
local function on_keypress_section(key, section_match, bindings)
    -- no section in the cursor
    if section_match == nil then
        return false
    end

    local binding = bindings[section_match.section_index]

    if binding == nil then
        return false
    end

    execute_binding(key, binding, section_match.section_content_current_line, section_match.cursor_col)
    return true
end

local function on_keypress_view(key, binding)
    execute_binding(key, binding)
end

function M.on_keypress(key, section_index)
    local section_bindings = M.State.section_bindings[key]

    if section_bindings ~= nil then
        if on_keypress_section(key, section_index, section_bindings) then
            return
        end
    end

    local view_bindings = M.State.view_bindings[key]

    if view_bindings ~= nil then
        on_keypress_view(key, view_bindings)
    end
end

return M


================================================
FILE: lua/sidebar-nvim/builtin/buffers.lua
================================================
local utils = require("sidebar-nvim.utils")
local view = require("sidebar-nvim.view")
local Loclist = require("sidebar-nvim.components.loclist")
local config = require("sidebar-nvim.config")
local has_devicons, devicons = pcall(require, "nvim-web-devicons")

local loclist = Loclist:new({ omit_single_group = true })
local loclist_items = {}

local function get_fileicon(filename)
    if has_devicons and devicons.has_loaded() then
        local extension = filename:match("^.+%.(.+)$")

        local fileicon = ""
        local icon, highlight = devicons.get_icon(filename, extension)
        if icon then
            fileicon = icon
        end

        if not highlight then
            highlight = "SidebarNvimNormal"
        end

        return {
            text = "  " .. fileicon .. " ",
            hl = highlight,
        }
    else
        return { text = "   " }
    end
end

local function get_buffers(ctx)
    local lines = {}
    local hl = {}
    local current_buffer = vim.api.nvim_get_current_buf()
    loclist_items = {}

    for _, buffer in ipairs(vim.api.nvim_list_bufs()) do
        if buffer ~= view.View.bufnr then
            local ignored = false
            local bufname = vim.api.nvim_buf_get_name(buffer)

            for _, ignored_buffer in ipairs(config.buffers.ignored_buffers or {}) do
                if string.match(bufname, ignored_buffer) then
                    ignored = true
                end
            end

            if bufname == "" then
                ignored = true
            end

            if config.buffers.ignore_not_loaded and not vim.api.nvim_buf_is_loaded(buffer) then
                ignored = true
            end

            -- NOTE: should we be more specific?
            if vim.api.nvim_buf_get_option(buffer, "bufhidden") ~= "" then
                ignored = true
            end

            -- always ignore terminals
            if config.buffers.ignore_terminal and string.match(bufname, "term://.*") then
                ignored = true
            end

            if not ignored then
                local name_hl = "SidebarNvimNormal"
                local modified = ""

                if buffer == current_buffer then
                    name_hl = "SidebarNvimBuffersActive"
                end

                if vim.api.nvim_buf_get_option(buffer, "modified") then
                    modified = " *"
                end

                -- sorting = "id"
                local order = buffer
                if config["buffers"].sorting == "name" then
                    order = bufname
                end

                local numbers_text = {}
                if config.buffers.show_numbers then
                    numbers_text = { text = buffer .. " ", hl = "SidebarNvimBuffersNumber" }
                end

                loclist_items[#loclist_items + 1] = {
                    group = "buffers",
                    left = {
                        get_fileicon(bufname),
                        numbers_text,
                        { text = utils.filename(bufname) .. modified, hl = name_hl },
                    },
                    data = { buffer = buffer, filepath = bufname },
                    order = order,
                }
            end
        end
    end

    loclist:set_items(loclist_items, { remove_groups = false })
    loclist:draw(ctx, lines, hl)

    if lines == nil or #lines == 0 then
        return "<no buffers>"
    else
        return { lines = lines, hl = hl }
    end
end

return {
    title = "Buffers",
    icon = config["buffers"].icon,
    draw = function(ctx)
        return get_buffers(ctx)
    end,
    highlights = {
        groups = {},
        links = {
            SidebarNvimBuffersActive = "SidebarNvimSectionTitle",
            SidebarNvimBuffersNumber = "SIdebarnvimLineNr",
        },
    },
    bindings = {
        ["d"] = function(line)
            local location = loclist:get_location_at(line)

            if location == nil then
                return
            end

            local buffer = location.data.buffer
            local is_modified = vim.api.nvim_buf_get_option(buffer, "modified")

            if is_modified then
                local action = vim.fn.input(
                    'file "' .. location.data.filepath .. '" has been modified. [w]rite/[d]iscard/[c]ancel: '
                )

                if action == "w" then
                    vim.api.nvim_buf_call(buffer, function()
                        vim.cmd("silent! w")
                    end)
                    vim.api.nvim_buf_delete(buffer, { force = true })
                elseif action == "d" then
                    vim.api.nvim_buf_delete(buffer, { force = true })
                end
            else
                vim.api.nvim_buf_delete(buffer, { force = true })
            end
        end,
        ["e"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end

            vim.cmd("wincmd p")
            vim.cmd("e " .. location.data.filepath)
        end,
        ["w"] = function(line)
            local location = loclist:get_location_at(line)

            if location == nil then
                return
            end

            vim.api.nvim_buf_call(location.data.buffer, function()
                vim.cmd("silent! w")
            end)
        end,
    },
}


================================================
FILE: lua/sidebar-nvim/builtin/containers.lua
================================================
local utils = require("sidebar-nvim.utils")
local docker_utils = require("sidebar-nvim.docker_utils")
local Loclist = require("sidebar-nvim.components.loclist")
local Debouncer = require("sidebar-nvim.debouncer")
local config = require("sidebar-nvim.config")
local luv = vim.loop

local loclist = Loclist:new({
    omit_single_group = true,
})

local output_tmp = ""

local function get_container_icon(container)
    local default = { hl = "SidebarNvimDockerContainerStatusRunning", text = "✓" }

    local mapping = { running = default, exited = { hl = "SidebarNvimDockerContainerStatusExited", text = "☒" } }

    local icon = mapping[container.State] or default

    return icon
end

local state_order_mapping = { running = 0, exited = 1 }

local function async_update(_)
    local stdout = luv.new_pipe(false)
    local stderr = luv.new_pipe(false)

    local handle

    local args = { "ps" }
    if config.containers.show_all then
        args = { "ps", "-a" }
    end

    local cmd = docker_utils.build_docker_command(args, stdout, stderr)
    handle = luv.spawn(cmd.bin, cmd.opts, function()
        vim.schedule(function()
            local loclist_items = {}
            if output_tmp ~= "" then
                for _, line in ipairs(vim.split(output_tmp, "\n")) do
                    line = string.sub(line, 2, #line - 1)
                    if line ~= "" then
                        -- TODO: on nightly change `vim.fn.json_*` to `vim.json_decode`, which is way faster and no need for schedule wrap
                        local ret, container = pcall(vim.fn.json_decode, line)
                        if ret then
                            local icon = get_container_icon(container)
                            table.insert(loclist_items, {
                                group = "containers",
                                left = {
                                    { text = icon.text .. " ", hl = icon.hl },
                                    { text = container.Names },
                                },
                                order = state_order_mapping[container.State] or 999,
                                id = container.ID,
                            })
                        else
                            vim.schedule(function()
                                utils.echo_warning("invalid container output: " .. container)
                            end)
                        end
                    end
                end
            end
            loclist:set_items(loclist_items, { remove_groups = false })
        end)

        luv.read_stop(stdout)
        luv.read_stop(stderr)
        stdout:close()
        stderr:close()
        handle:close()
    end)

    output_tmp = ""

    luv.read_start(stdout, function(err, data)
        if data == nil then
            return
        end

        output_tmp = output_tmp .. data

        if err ~= nil then
            vim.schedule(function()
                utils.echo_warning(err)
            end)
        end
    end)

    luv.read_start(stderr, function(err, data)
        if data == nil then
            return
        end

        if err ~= nil then
            vim.schedule(function()
                utils.echo_warning(err)
            end)
        end
    end)
end

local async_update_debounced

return {
    title = "Containers",
    icon = config.containers.icon,
    setup = function()
        local interval = config.containers.interval or 2000
        async_update_debounced = Debouncer:new(async_update, interval)
        async_update_debounced:call()
    end,
    update = function(ctx)
        async_update_debounced:call(ctx)
    end,
    draw = function(ctx)
        async_update_debounced:call(ctx)

        local lines = {}
        local hl = {}

        loclist:draw(ctx, lines, hl)

        if #lines == 0 then
            lines = { "<no containers>" }
        end

        return { lines = lines, hl = hl }
    end,
    highlights = {
        groups = {},
        links = {
            SidebarNvimDockerContainerStatusRunning = "LspDiagnosticsDefaultInformation",
            SidebarNvimDockerContainerStatusExited = "LspDiagnosticsDefaultError",
        },
    },
    bindings = {
        ["e"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end
            vim.cmd("wincmd p")
            vim.cmd("terminal " .. docker_utils.build_docker_attach_command(location.id))
        end,
    },
}


================================================
FILE: lua/sidebar-nvim/builtin/datetime.lua
================================================
local config = require("sidebar-nvim.config")
local utils = require("sidebar-nvim.utils")
local has_luatz, luatz = pcall(require, "luatz")
local _, timetable = pcall(require, "luatz.timetable")

local is_config_valid = false
local config_error_messages = {}

local function get_clock_value_using_luatz(clock, format)
    local dt = luatz.time()

    local tzinfo = luatz.get_tz(clock.tz)
    if tzinfo then
        dt = tzinfo:localise(dt)
    else
        utils.echo_warning(string.format("tz '%s' not found", clock.tz))
    end

    return luatz.strftime.strftime(format, timetable.new_from_timestamp(dt))
end

local function validate_config()
    if not config.datetime or not config.datetime.clocks or #config.datetime.clocks == 0 then
        is_config_valid = true
        return
    end

    for _, clock in ipairs(config.datetime.clocks) do
        if clock.tz then
            if not has_luatz then
                utils.echo_warning("luatz not installed. Cannot use 'tz' option without luatz")
                config_error_messages = { "luatz not installed.", "Cannot use 'tz' option without luatz" }
                is_config_valid = false
                return
            end
        end
    end

    is_config_valid = true
end

return {
    title = "Current datetime",
    icon = config.datetime.icon,
    setup = function()
        validate_config()
    end,
    draw = function()
        local lines = {}
        local hl = {}

        if not is_config_valid then
            for _, msg in ipairs(config_error_messages) do
                table.insert(lines, msg)
            end
            return { lines = lines, hl = hl }
        end

        if not config.datetime or not config.datetime.clocks or #config.datetime.clocks == 0 then
            table.insert(lines, "<no clocks>")
            return { lines = lines, hl = hl }
        end

        local clocks_num = #config.datetime.clocks
        for i, clock in ipairs(config.datetime.clocks) do
            local format = clock.format or config.datetime.format

            local clock_value
            if has_luatz then
                clock_value = get_clock_value_using_luatz(clock, format)
            else
                local offset = clock.offset or 0
                clock_value = os.date(format, os.time() + offset * 60 * 60)
            end

            table.insert(hl, { "SidebarNvimDatetimeClockName", #lines, 0, -1 })
            table.insert(lines, "# " .. (clock.name or clock.offset or clock.tz))

            table.insert(hl, { "SidebarNvimDatetimeClockValue", #lines, 0, -1 })
            table.insert(lines, clock_value)

            if i < clocks_num then
                table.insert(lines, "")
            end
        end

        return { lines = lines, hl = hl }
    end,
    highlights = {
        groups = {},
        links = {
            SidebarNvimDatetimeClockName = "SidebarNvimComment",
            SidebarNvimDatetimeClockValue = "SidebarNvimNormal",
        },
    },
}


================================================
FILE: lua/sidebar-nvim/builtin/diagnostics.lua
================================================
local Loclist = require("sidebar-nvim.components.loclist")
local config = require("sidebar-nvim.config")

local loclist = Loclist:new({})

local severity_level = { "Error", "Warning", "Info", "Hint" }
local icons = { "", "", "", "" }
local use_icons = true

local function get_diagnostics()
    local current_buf = vim.api.nvim_get_current_buf()
    local current_buf_filepath = vim.api.nvim_buf_get_name(current_buf)
    local current_buf_filename = vim.fn.fnamemodify(current_buf_filepath, ":t")

    local open_bufs = vim.api.nvim_list_bufs()

    local all_diagnostics = vim.diagnostic.get()
    local loclist_items = {}

    for _, diag in pairs(all_diagnostics) do
        local bufnr = diag.bufnr
        if open_bufs[bufnr] ~= nil and vim.api.nvim_buf_is_loaded(bufnr) then
            local filepath = vim.api.nvim_buf_get_name(bufnr)
            local filename = vim.fn.fnamemodify(filepath, ":t")

            local message = diag.message
            message = message:gsub("\n", " ")

            local severity = diag.severity
            local level = severity_level[severity]
            local icon = icons[severity]

            if not use_icons then
                icon = level
            end

            table.insert(loclist_items, {
                group = filename,
                left = {
                    { text = icon .. " ", hl = "SidebarNvimLspDiagnostics" .. level },
                    {
                        text = diag.lnum + 1,
                        hl = "SidebarNvimLspDiagnosticsLineNumber",
                    },
                    { text = ":" },
                    {
                        text = (diag.col + 1) .. " ",
                        hl = "SidebarNvimLspDiagnosticsColNumber",
                    },
                    { text = message },
                },
                lnum = diag.lnum + 1,
                col = diag.col + 1,
                filepath = filepath,
            })
        end
    end

    local previous_state = vim.tbl_map(function(group)
        return group.is_closed
    end, loclist.groups)

    loclist:set_items(loclist_items, { remove_groups = true })
    loclist:close_all_groups()

    for group_name, is_closed in pairs(previous_state) do
        if loclist.groups[group_name] ~= nil then
            loclist.groups[group_name].is_closed = is_closed
        end
    end

    if loclist.groups[current_buf_filename] ~= nil then
        loclist.groups[current_buf_filename].is_closed = false
    end
end

return {
    title = "Diagnostics",
    icon = config["diagnostics"].icon,
    setup = function(_)
        vim.api.nvim_exec(
            [[
          augroup sidebar_nvim_diagnostics_update
              autocmd!
              autocmd DiagnosticChanged * lua require'sidebar-nvim.builtin.diagnostics'.update()
          augroup END
          ]],
            false
        )

        get_diagnostics()
    end,
    update = function(_)
        get_diagnostics()
    end,
    draw = function(ctx)
        local lines = {}
        local hl = {}

        loclist:draw(ctx, lines, hl)

        if lines == nil or #lines == 0 then
            return "<no diagnostics>"
        else
            return { lines = lines, hl = hl }
        end
    end,
    highlights = {
        groups = {},
        links = {
            SidebarNvimLspDiagnosticsError = "LspDiagnosticsDefaultError",
            SidebarNvimLspDiagnosticsWarning = "LspDiagnosticsDefaultWarning",
            SidebarNvimLspDiagnosticsInfo = "LspDiagnosticsDefaultInformation",
            SidebarNvimLspDiagnosticsHint = "LspDiagnosticsDefaultHint",
            SidebarNvimLspDiagnosticsLineNumber = "SidebarNvimLineNr",
            SidebarNvimLspDiagnosticsColNumber = "SidebarNvimLineNr",
        },
    },
    bindings = {
        ["t"] = function(line)
            loclist:toggle_group_at(line)
        end,
        ["e"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end
            -- TODO: I believe there is a better way to do this, but I haven't had the time to do investigate
            vim.cmd("wincmd p")
            vim.cmd("e " .. location.filepath)
            vim.fn.cursor(location.lnum, location.col)
        end,
    },
}


================================================
FILE: lua/sidebar-nvim/builtin/files.lua
================================================
local utils = require("sidebar-nvim.utils")
local Loclist = require("sidebar-nvim.components.loclist")
local config = require("sidebar-nvim.config")
local has_devicons, devicons = pcall(require, "nvim-web-devicons")
local luv = vim.loop

local loclist = Loclist:new({ omit_single_group = false, show_group_count = false })

local icons = {
    directory_closed = "",
    directory_open = "",
    file = "",
    closed = "",
    opened = "",
}

local yanked_files = {}
local cut_files = {}
local open_directories = {}

local history = { position = 0, groups = {} }
local trash_dir = luv.os_homedir() .. "/.local/share/Trash/files/"

local function get_fileicon(filename)
    if has_devicons and devicons.has_loaded() then
        local extension = filename:match("^.+%.(.+)$")
        local fileicon, highlight = devicons.get_icon(filename, extension)

        if not highlight then
            highlight = "SidebarNvimNormal"
        end
        return { text = fileicon or icons["file"], hl = highlight }
    end
    return { text = icons["file"] .. " " }
end

-- scan directory recursively
local function scan_dir(directory)
    if not open_directories[directory] then
        return
    end

    local show_hidden = config.files.show_hidden
    local handle = luv.fs_scandir(directory)
    local children = {}
    local children_directories = {}
    local children_files = {}

    while handle do
        local filename, filetype = luv.fs_scandir_next(handle)

        if not filename then
            break
        end

        local path = directory .. "/" .. filename
        local ignored = false

        for _, ignored_path in ipairs(config.files.ignored_paths or {}) do
            if string.match(path, ignored_path) then
                ignored = true
            end
        end

        if not ignored then
            if show_hidden or filename:sub(1, 1) ~= "." then
                if filetype == "file" then
                    table.insert(children_files, {
                        name = filename,
                        type = "file",
                        path = path,
                        parent = directory,
                    })
                elseif filetype == "directory" then
                    table.insert(children_directories, {
                        name = filename,
                        type = "directory",
                        path = path,
                        parent = directory,
                        children = scan_dir(directory .. "/" .. filename),
                    })
                end
            end
        end
    end

    table.sort(children_directories, function(a, b)
        return a.name < b.name
    end)
    vim.list_extend(children, children_directories)

    table.sort(children_files, function(a, b)
        return a.name < b.name
    end)
    vim.list_extend(children, children_files)

    return children
end

local function build_loclist(group, directory, level)
    local loclist_items = {}

    if directory.children then
        for _, node in ipairs(directory.children) do
            if node.type == "file" then
                local icon = get_fileicon(node.name)
                local selected = { text = "" }

                if yanked_files[node.path] then
                    selected = { text = " *", hl = "SidebarNvimFilesYanked" }
                elseif cut_files[node.path] then
                    selected = { text = " *", hl = "SidebarNvimFilesCut" }
                end

                loclist_items[#loclist_items + 1] = {
                    group = group,
                    left = {
                        { text = string.rep("  ", level) .. icon.text .. " ", hl = icon.hl },
                        { text = node.name },
                        selected,
                    },
                    name = node.name,
                    path = node.path,
                    type = node.type,
                    parent = node.parent,
                    node = node,
                }
            elseif node.type == "directory" then
                local icon
                if open_directories[node.path] then
                    icon = icons["directory_open"]
                else
                    icon = icons["directory_closed"]
                end

                local selected = { text = "" }

                if yanked_files[node.path] then
                    selected = { text = " *", hl = "SidebarNvimFilesYanked" }
                elseif cut_files[node.path] then
                    selected = { text = " *", hl = "SidebarNvimFilesCut" }
                end

                loclist_items[#loclist_items + 1] = {
                    group = group,
                    left = {
                        {
                            text = string.rep("  ", level) .. icon .. " " .. node.name,
                            hl = "SidebarNvimFilesDirectory",
                        },
                        selected,
                    },
                    name = node.name,
                    path = node.path,
                    type = node.type,
                    parent = node.parent,
                    node = node,
                }
            end

            if node.type == "directory" and open_directories[node.path] then
                vim.list_extend(loclist_items, build_loclist(group, node, level + 1))
            end
        end
    end
    return loclist_items
end

local function update(group, directory)
    local node = { path = directory, children = scan_dir(directory) }

    loclist:set_items(build_loclist(group, node, 0), { remove_groups = true })
end

local function exec(group)
    for _, op in ipairs(group.operations) do
        op.exec()
    end

    group.executed = true
end

-- undo the operation
local function undo(group)
    for _, op in ipairs(group.operations) do
        op.undo()
    end
end

local function copy_file(src, dest, confirm_overwrite)
    if confirm_overwrite and luv.fs_access(dest, "r") ~= false then
        local overwrite = vim.fn.input('file "' .. dest .. '" already exists. Overwrite? y/n: ')

        if overwrite ~= "y" then
            return
        end
    end

    luv.fs_copyfile(src, dest, function(err, _)
        if err ~= nil then
            vim.schedule(function()
                utils.echo_warning(err)
            end)
        end
    end)
end

local function create_file(dest)
    if luv.fs_access(dest, "r") ~= false then
        vim.schedule(function()
            utils.echo_warning('file "' .. dest .. '" already exists.')
        end)
        return
    end

    local is_file = not dest:match("/$")
    local parent_folders = vim.fn.fnamemodify(dest, ":h")

    if not utils.file_exist(parent_folders) then
        local success = vim.fn.mkdir(parent_folders, "p")
        if not success then
            utils.echo_warning("Could not create directory " .. parent_folders)
        end
    end

    if is_file then
        luv.fs_open(dest, "w", 420, function(err, file)
            if err ~= nil then
                vim.schedule(function()
                    utils.echo_warning(err)
                end)
            else
                luv.fs_close(file)
            end
        end)
    end
end

local function delete_file(src, trash, confirm_deletion)
    if confirm_deletion then
        local delete = vim.fn.input('delete file "' .. src .. '"? y/n: ')

        if delete ~= "y" then
            return
        end
    end

    luv.fs_rename(src, trash, function(err, _)
        if err ~= nil then
            vim.schedule(function()
                utils.echo_warning(err)
            end)
        end
    end)
end

local function move_file(src, dest, confirm_overwrite)
    if confirm_overwrite and luv.fs_access(dest, "r") ~= false then
        local overwrite = vim.fn.input('file "' .. dest .. '" already exists. Overwrite? y/n: ')

        if overwrite ~= "y" then
            return
        end
    end

    luv.fs_rename(src, dest, function(err, _)
        if err ~= nil then
            vim.schedule(function()
                utils.echo_warning(err)
            end)
        end
    end)
end

return {
    title = "Files",
    icon = config["files"].icon,
    setup = function(_)
        vim.api.nvim_exec(
            [[
          augroup sidebar_nvim_files_update
              autocmd!
              autocmd ShellCmdPost * lua require'sidebar-nvim.builtin.files'.update()
              autocmd BufLeave term://* lua require'sidebar-nvim.builtin.files'.update()
          augroup END
          ]],
            false
        )
    end,
    update = function(_)
        local cwd = vim.fn.getcwd()
        local group = utils.shortest_path(cwd)

        open_directories[cwd] = true

        update(group, cwd)
    end,
    draw = function(ctx)
        local lines = {}
        local hl = {}

        loclist:draw(ctx, lines, hl)

        return { lines = lines, hl = hl }
    end,

    highlights = {
        groups = {},
        links = {
            SidebarNvimFilesDirectory = "SidebarNvimSectionTitle",
            SidebarNvimFilesYanked = "SidebarNvimLabel",
            SidebarNvimFilesCut = "DiagnosticError",
        },
    },

    bindings = {
        -- delete
        ["d"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end

            local operation
            operation = {
                exec = function()
                    delete_file(operation.src, operation.dest, true)
                end,
                undo = function()
                    move_file(operation.dest, operation.src, true)
                end,
                src = location.node.path,
                dest = trash_dir .. location.node.name,
            }
            local group = { executed = false, operations = { operation } }

            history.position = history.position + 1
            history.groups = vim.list_slice(history.groups, 1, history.position)
            history.groups[history.position] = group

            exec(group)
        end,
        -- yank
        ["y"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end

            yanked_files[location.node.path] = true
            cut_files = {}
        end,
        -- cut
        ["x"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end

            cut_files[location.node.path] = true
            yanked_files = {}
        end,
        -- paste
        ["p"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end

            local dest_dir

            if location.node.type == "directory" then
                dest_dir = location.node.path
            else
                dest_dir = location.node.parent
            end

            open_directories[dest_dir] = true

            local group = { executed = false, operations = {} }

            for path, _ in pairs(yanked_files) do
                local operation
                operation = {
                    exec = function()
                        copy_file(operation.src, operation.dest, true)
                    end,
                    undo = function()
                        delete_file(operation.dest, trash_dir .. utils.filename(operation.src), true)
                    end,
                    src = path,
                    dest = dest_dir .. "/" .. utils.filename(path),
                }
                table.insert(group.operations, operation)
            end

            for path, _ in pairs(cut_files) do
                local operation
                operation = {
                    exec = function()
                        move_file(operation.src, operation.dest, true)
                    end,
                    undo = function()
                        move_file(operation.dest, operation.src, true)
                    end,
                    src = path,
                    dest = dest_dir .. "/" .. utils.filename(path),
                }
                table.insert(group.operations, operation)
            end
            history.position = history.position + 1
            history.groups = vim.list_slice(history.groups, 1, history.position)
            history.groups[history.position] = group

            yanked_files = {}
            cut_files = {}

            exec(group)
        end,
        -- create
        ["c"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end

            local parent

            if location.type == "directory" then
                parent = location.path
            else
                parent = location.parent
            end

            open_directories[parent] = true

            local name = vim.fn.input("file name: ")

            if string.len(vim.trim(name)) == 0 then
                return
            end

            local operation

            operation = {
                success = true,
                exec = function()
                    create_file(operation.dest)
                end,
                undo = function()
                    delete_file(operation.dest, trash_dir .. name, true)
                end,
                src = nil,
                dest = parent .. "/" .. name,
            }

            local group = { executed = false, operations = { operation } }

            history.position = history.position + 1
            history.groups = vim.list_slice(history.groups, 1, history.position)
            history.groups[history.position] = group

            exec(group)
        end,
        -- open current file
        ["e"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end

            if location.type == "file" then
                vim.cmd("wincmd p")
                vim.cmd("e " .. location.node.path)
            else
                if open_directories[location.node.path] == nil then
                    open_directories[location.node.path] = true
                else
                    open_directories[location.node.path] = nil
                end
            end
        end,
        -- rename
        ["r"] = function(line)
            local location = loclist:get_location_at(line)

            if location == nil then
                return
            end

            local new_name = vim.fn.input('rename file "' .. location.node.name .. '" to: ')
            local operation

            operation = {
                exec = function()
                    move_file(operation.src, operation.dest, true)
                end,
                undo = function()
                    move_file(operation.dest, operation.src, true)
                end,
                src = location.node.path,
                dest = location.node.parent .. "/" .. new_name,
            }

            local group = { executed = false, operations = { operation } }

            history.position = history.position + 1
            history.groups = vim.list_slice(history.groups, 1, history.position)
            history.groups[history.position] = group

            exec(group)
        end,
        -- undo
        ["u"] = function(_)
            if history.position > 0 then
                undo(history.groups[history.position])
                history.position = history.position - 1
            end
        end,
        -- redo
        ["<C-r>"] = function(_)
            if history.position < #history.groups then
                history.position = history.position + 1
                exec(history.groups[history.position])
            end
        end,
        ["<CR>"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end
            if location.node.type == "file" then
                vim.cmd("wincmd p")
                vim.cmd("e " .. location.node.path)
            else
                if open_directories[location.node.path] == nil then
                    open_directories[location.node.path] = true
                else
                    open_directories[location.node.path] = nil
                end
            end
        end,
    },
}


================================================
FILE: lua/sidebar-nvim/builtin/git-status.lua
================================================
local utils = require("sidebar-nvim.utils")

local git = require("sidebar-nvim.builtin.git")

local deprecated_git = vim.tbl_deep_extend("force", git, {
    setup = function(ctx)
        utils.echo_warning("Section 'git-status' is deprecated. Please use 'git'")

        if git.setup ~= nil then
            return git.setup(ctx)
        end
    end,
})

return deprecated_git


================================================
FILE: lua/sidebar-nvim/builtin/git.lua
================================================
local utils = require("sidebar-nvim.utils")
local sidebar = require("sidebar-nvim")
local Loclist = require("sidebar-nvim.components.loclist")
local Debouncer = require("sidebar-nvim.debouncer")
local config = require("sidebar-nvim.config")
local luv = vim.loop
local has_devicons, devicons = pcall(require, "nvim-web-devicons")

local loclist = Loclist:new({})

-- Make sure all groups exist
loclist:add_group("Staged")
loclist:add_group("Unstaged")
loclist:add_group("Unmerged")
loclist:add_group("Untracked")

local loclist_items = {}
local finished = 0
local expected_job_count = 4

-- parse line from git diff --numstat into a loclist item
local function parse_git_diff(group, line)
    local t = vim.split(line, "\t")
    local added, removed, filepath = t[1], t[2], t[3]
    local extension = filepath:match("^.+%.(.+)$")
    local fileicon = ""
    local filehighlight = "SidebarNvimGitStatusFileIcon"

    if has_devicons and devicons.has_loaded() then
        local icon, highlight = devicons.get_icon(filepath, extension)

        if icon then
            fileicon = icon
            filehighlight = highlight
        end
    end

    if filepath ~= "" then
        loclist:open_group(group)

        table.insert(loclist_items, {
            group = group,
            left = {
                {
                    text = fileicon .. " ",
                    hl = filehighlight,
                },
                {
                    text = utils.shortest_path(filepath) .. " ",
                    hl = "SidebarNvimGitStatusFileName",
                },
                {
                    text = added,
                    hl = "SidebarNvimGitStatusDiffAdded",
                },
                {
                    text = ", ",
                },
                {
                    text = removed,
                    hl = "SidebarNvimGitStatusDiffRemoved",
                },
            },
            filepath = filepath,
        })
    end
end

-- parse line from git status --porcelain into a loclist item
local function parse_git_status(group, line)
    local striped = line:match("^%s*(.-)%s*$")
    local status = striped:sub(0, 2)
    local filepath = striped:sub(3, -1):match("^%s*(.-)%s*$")
    local extension = filepath:match("^.+%.(.+)$")

    if status == "??" then
        local fileicon = ""

        if has_devicons and devicons.has_loaded() then
            local icon = vim.schedule_wrap(function()
                return devicons.get_icon_color(filepath, extension)
            end)()
            if icon then
                fileicon = icon
            end
        end

        loclist:open_group(group)

        table.insert(loclist_items, {
            group = group,
            left = {
                {
                    text = fileicon .. " ",
                    hl = "SidebarNvimGitStatusFileIcon",
                },
                {
                    text = utils.shortest_path(filepath),
                    hl = "SidebarNvimGitStatusFileName",
                },
            },
            filepath = filepath,
        })
    end
end

-- execute async command and parse result into loclist items
local function async_cmd(group, command, args, parse_fn)
    local stdout = luv.new_pipe(false)
    local stderr = luv.new_pipe(false)

    local handle
    handle = luv.spawn(command, { args = args, stdio = { nil, stdout, stderr }, cwd = luv.cwd() }, function()
        finished = finished + 1

        if finished == expected_job_count then
            loclist:set_items(loclist_items, { remove_groups = false })
        end

        luv.read_stop(stdout)
        luv.read_stop(stderr)
        stdout:close()
        stderr:close()
        handle:close()
    end)

    luv.read_start(stdout, function(err, data)
        if data == nil then
            return
        end

        for _, line in ipairs(vim.split(data, "\n")) do
            if line ~= "" then
                parse_fn(group, line)
            end
        end

        if err ~= nil then
            vim.schedule(function()
                utils.echo_warning(err)
            end)
        end
    end)

    luv.read_start(stderr, function(err, data)
        if data == nil then
            return
        end

        if err ~= nil then
            vim.schedule(function()
                utils.echo_warning(err)
            end)
        end
    end)
end

local function async_update(_)
    loclist_items = {}
    finished = 0

    -- if add a new job, please update `expected_job_count` at the top
    -- TODO: investigate using coroutines to wait for all jobs and then update the loclist
    async_cmd("Staged", "git", { "diff", "--numstat", "--staged", "--diff-filter=u" }, parse_git_diff)
    async_cmd("Unstaged", "git", { "diff", "--numstat", "--diff-filter=u" }, parse_git_diff)
    async_cmd("Unmerged", "git", { "diff", "--numstat", "--diff-filter=U" }, parse_git_diff)
    async_cmd("Untracked", "git", { "status", "--porcelain" }, parse_git_status)
end

local async_update_debounced = Debouncer:new(async_update, 1000)

return {
    title = "Git Status",
    icon = config["git"].icon,
    setup = function(ctx)
        -- ShellCmdPost triggered after ":!<cmd>"
        -- BufLeave triggered only after leaving terminal buffers
        vim.api.nvim_exec(
            [[
          augroup sidebar_nvim_git_status_update
              autocmd!
              autocmd ShellCmdPost * lua require'sidebar-nvim.builtin.git'.update()
              autocmd BufLeave term://* lua require'sidebar-nvim.builtin.git'.update()
          augroup END
          ]],
            false
        )
        async_update_debounced:call(ctx)
    end,
    update = function(ctx)
        if not ctx then
            ---@diagnostic disable-next-line: missing-parameter
            ctx = { width = sidebar.get_width() }
        end
        async_update_debounced:call(ctx)
    end,
    draw = function(ctx)
        local lines = {}
        local hl = {}

        loclist:draw(ctx, lines, hl)

        if #lines == 0 then
            lines = { "<no changes>" }
        end

        return { lines = lines, hl = hl }
    end,
    highlights = {
        groups = {},
        links = {
            SidebarNvimGitStatusFileName = "SidebarNvimNormal",
            SidebarNvimGitStatusFileIcon = "SidebarNvimSectionTitle",
            SidebarNvimGitStatusDiffAdded = "DiffAdded",
            SidebarNvimGitStatusDiffRemoved = "DiffRemoved",
        },
    },
    bindings = {
        ["e"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end
            vim.cmd("wincmd p")
            vim.cmd("e " .. location.filepath)
        end,
        -- stage files
        ["s"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end

            utils.async_cmd("git", { "add", location.filepath }, function()
                async_update_debounced:call()
            end)
        end,
        -- unstage files
        ["u"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end

            utils.async_cmd("git", { "restore", "--staged", location.filepath }, function()
                async_update_debounced:call()
            end)
        end,
    },
}


================================================
FILE: lua/sidebar-nvim/builtin/lsp-diagnostics.lua
================================================
local utils = require("sidebar-nvim.utils")

local diagnostics = require("sidebar-nvim.builtin.diagnostics")

local deprecated_diagnostics = vim.tbl_deep_extend("force", diagnostics, {
    setup = function(ctx)
        utils.echo_warning("Section 'lsp-diagnostics' is deprecated. Please use 'diagnostics'")

        if diagnostics.setup ~= nil then
            return diagnostics.setup(ctx)
        end
    end,
})

return deprecated_diagnostics


================================================
FILE: lua/sidebar-nvim/builtin/symbols.lua
================================================
local Loclist = require("sidebar-nvim.components.loclist")
local config = require("sidebar-nvim.config")
local view = require("sidebar-nvim.view")

local loclist = Loclist:new({ omit_single_group = true })
local open_symbols = {}
local last_buffer
local last_pos

local kinds = {
    { text = " ", hl = "TSURI" },
    { text = " ", hl = "TSNamespace" },
    { text = " ", hl = "TSNamespace" },
    { text = " ", hl = "TSNamespace" },
    { text = "𝓒 ", hl = "TSType" },
    { text = "ƒ ", hl = "TSMethod" },
    { text = " ", hl = "TSMethod" },
    { text = " ", hl = "TSField" },
    { text = " ", hl = "TSConstructor" },
    { text = "ℰ ", hl = "TSType" },
    { text = "ﰮ ", hl = "TSType" },
    { text = " ", hl = "TSFunction" },
    { text = " ", hl = "TSConstant" },
    { text = " ", hl = "TSConstant" },
    { text = "𝓐 ", hl = "TSString" },
    { text = "# ", hl = "TSNumber" },
    { text = "⊨ ", hl = "TSBoolean" },
    { text = " ", hl = "TSConstant" },
    { text = "⦿ ", hl = "TSType" },
    { text = " ", hl = "TSType" },
    { text = "NULL ", hl = "TSType" },
    { text = " ", hl = "TSField" },
    { text = "𝓢 ", hl = "TSType" },
    { text = "🗲 ", hl = "TSType" },
    { text = "+ ", hl = "TSOperator" },
    { text = "𝙏 ", hl = "TSParameter" },
}

local function get_range(s)
    return s.range or s.location.range
end

local function build_loclist(filepath, loclist_items, symbols, level)
    table.sort(symbols, function(a, _)
        return get_range(a).start.line < get_range(a).start.line
    end)

    for _, symbol in ipairs(symbols) do
        local kind = kinds[symbol.kind]
        loclist_items[#loclist_items + 1] = {
            group = "symbols",
            left = {
                { text = string.rep(" ", level) .. kind.text, hl = kind.hl },
                { text = symbol.name .. " ", hl = "SidebarNvimSymbolsName" },
                { text = symbol.detail, hl = "SidebarNvimSymbolsDetail" },
            },
            right = {},
            data = { symbol = symbol, filepath = filepath },
        }

        -- uses a unique key for each symbol appending the name and position
        if symbol.children and open_symbols[symbol.name .. symbol.range.start.line .. symbol.range.start.character] then
            build_loclist(filepath, loclist_items, symbol.children, level + 1)
        end
    end
end

local function get_symbols(_)
    local current_buf = vim.api.nvim_get_current_buf()
    local current_pos = vim.lsp.util.make_position_params()

    -- if current buffer is sidebar's own buffer, use previous buffer
    if current_buf ~= view.View.bufnr then
        last_buffer = current_buf
        last_pos = current_pos
    else
        current_buf = last_buffer
        current_pos = last_pos
    end

    if
        current_buf == view.View.bufnr
        or not current_buf
        or not vim.api.nvim_buf_is_loaded(current_buf)
        or vim.api.nvim_buf_get_option(current_buf, "buftype") ~= ""
    then
        loclist:clear()
        return
    end

    local clients = vim.lsp.buf_get_clients(current_buf)
    local clients_filtered = vim.tbl_filter(function(client)
        return client.supports_method("textDocument/documentSymbol")
    end, clients)

    if #clients_filtered == 0 then
        loclist:clear()
        return
    end

    vim.lsp.buf_request(current_buf, "textDocument/documentSymbol", current_pos, function(err, method, symbols)
        if vim.fn.has("nvim-0.5.1") == 1 or vim.fn.has("nvim-0.8") == 1 then
            symbols = method
        end

        local loclist_items = {}
        local filepath = vim.api.nvim_buf_get_name(current_buf)
        if err ~= nil then
            return
        end

        if symbols ~= nil then
            build_loclist(filepath, loclist_items, symbols, 1)
            loclist:set_items(loclist_items, { remove_groups = false })
        end
    end)
end

return {
    title = "Symbols",
    icon = config["symbols"].icon,
    draw = function(ctx)
        local lines = {}
        local hl = {}

        get_symbols(ctx)

        loclist:draw(ctx, lines, hl)

        if lines == nil or #lines == 0 then
            return "<no symbols>"
        else
            return { lines = lines, hl = hl }
        end
    end,
    highlights = {
        groups = {},
        links = {
            SidebarNvimSymbolsName = "SidebarNvimNormal",
            SidebarNvimSymbolsDetail = "SidebarNvimLineNr",
        },
    },
    bindings = {
        ["t"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end
            local symbol = location.data.symbol
            local key = symbol.name .. symbol.range.start.line .. symbol.range.start.character

            if open_symbols[key] == nil then
                open_symbols[key] = true
            else
                open_symbols[key] = nil
            end
        end,
        ["e"] = function(line)
            local location = loclist:get_location_at(line)
            if location == nil then
                return
            end

            local symbol = location.data.symbol

            vim.cmd("wincmd p")
            vim.cmd("e " .. location.data.filepath)
            vim.fn.cursor(symbol.range.start.line + 1, symbol.range.start.character + 1)
        end,
    },
}


================================================
FILE: lua/sidebar-nvim/builtin/todos.lua
================================================
local utils = require("sidebar-nvim.utils")
local Loclist = require("sidebar-nvim.components.loclist")
local config = require("sidebar-nvim.config")
local luv = vim.loop

local loclist = Loclist:new({
    groups_initially_closed = config.todos.initially_closed,
    show_empty_groups = false,
})

-- Make sure all groups exist
loclist:add_group("TODO")
loclist:add_group("HACK")
loclist:add_group("WARN")
loclist:add_group("PERF")
loclist:add_group("NOTE")
loclist:add_group("FIX")

local icons = {
    TODO = { text = "", hl = "SidebarNvimTodoIconTodo" },
    HACK = { text = "", hl = "SidebarNvimTodoIconHack" },
    WARN = { text = "", hl = "SidebarNvimTodoIconWarn" },
    PERF = { text = "", hl = "SidebarNvimTodoIconPerf" },
    NOTE = { text = "", hl = "SidebarNvimTodoIconNote" },
    FIX = { text = "", hl = "SidebarNvimTodoIconFix" },
}

local current_path_ignored_cache = false

local function is_current_path_ignored()
    local cwd = vim.loop.cwd()
    for _, path in pairs(config.todos.ignored_paths or {}) do
        if vim.fn.expand(path) == cwd then
            return true
        end
    end

    return false
end

local function async_update(ctx)
    current_path_ignored_cache = is_current_path_ignored()
    if current_path_ignored_cache then
        return
    end

    local todos = {}

    local stdout = luv.new_pipe(false)
    local stderr = luv.new_pipe(false)
    local handle
    local cmd
    local args
    local keywords_regex = [[(TODO|NOTE|FIX|PERF|HACK|WARN)]]
    local regex_end = [[\s*(\(.*\))?:.*]]

    -- Use ripgrep by default, if it's installed
    if vim.fn.executable("rg") == 1 then
        cmd = "rg"
        args = {
            "--no-hidden",
            "--column",
            "--only-matching",
            keywords_regex .. regex_end,
        }
    else
        cmd = "git"
        args = { "grep", "-no", "--column", "-EI", keywords_regex .. regex_end }
    end

    handle = luv.spawn(cmd, {
        args = args,
        stdio = { nil, stdout, stderr },
        cmd = luv.cwd(),
    }, function()
        local loclist_items = {}
        for _, items in pairs(todos) do
            for _, item in ipairs(items) do
                table.insert(loclist_items, {
                    group = item.tag,
                    left = {
                        icons[item.tag],
                        { text = " " .. item.lnum, hl = "SidebarNvimTodoLineNumber" },
                        { text = ":" },
                        { text = item.col, hl = "SidebarNvimTodoColNumber" },
                        { text = utils.truncate(item.text, ctx.width / 2) },
                    },
                    right = {
                        {
                            text = utils.filename(item.filepath),
                            hl = "SidebarNvimLineNr",
                        },
                    },
                    filepath = item.filepath,
                    order = item.filepath,
                    lnum = item.lnum,
                    col = item.col,
                })
            end
        end
        loclist:set_items(loclist_items, { remove_groups = false })

        luv.read_stop(stdout)
        luv.read_stop(stderr)
        stdout:close()
        stderr:close()
        handle:close()
    end)

    luv.read_start(stdout, function(err, data)
        if data == nil then
            return
        end

        for _, line in ipairs(vim.split(data, "\n")) do
            if line ~= "" then
                local filepath, lnum, col, tag, text = line:match("^(.+):(%d+):(%d+):([%w%(%)]+):(.*)$")

                if filepath and tag then
                    local tag_with_scope = { tag:match("(%w+)%(.*%)") }
                    if #tag_with_scope > 0 then
                        tag = tag_with_scope[1]
                    end

                    if not todos[tag] then
                        todos[tag] = {}
                    end

                    local category_tbl = todos[tag]

                    category_tbl[#category_tbl + 1] = {
                        filepath = filepath,
                        lnum = lnum,
                        col = col,
                        tag = tag,
                        text = text,
                    }
                end
            end
        end

        if err ~= nil then
            vim.schedule(function()
                utils.echo_warning(err)
            end)
        end
    end)

    luv.read_start(stderr, function(err, data)
        if data == nil then
            return
        end

        if err ~= nil then
            vim.schedule(function()
                utils.echo_warning(err)
            end)
        end
    end)
end

return {
    title = "TODOs",
    icon = config.todos.icon,
    draw = function(ctx)
        local lines = {}
        local hl = {}

        if current_path_ignored_cache then
            lines = { "<path ignored>" }
        end

        loclist:draw(ctx, lines, hl)

        if #lines == 0 then
            lines = { "<no TODOs>" }
        end

        return { lines = lines, hl = hl }
    end,
    highlights = {
        groups = {},
        links = {
            SidebarNvimTodoFilename = "SidebarNvimLineNr",
            SidebarNvimTodoLineNumber = "SidebarNvimLineNr",
            SidebarNvimTodoColNumber = "SidebarNvimLineNr",
            SidebarNvimTodoIconTodo = "DiagnosticInfo",
            SidebarNvimTodoIconHack = "DiagnosticWarning",
            SidebarNvimTodoIconWarn = "DiagnosticWarning",
            SidebarNvimTodoIconPerf = "DiagnosticError",
            SidebarNvimTodoIconNote = "DiagnosticHint",
            SidebarNvimTodoIconFix = "DiagnosticError",
        },
    },
    bindings = {
        ["t"] = function(line)
            loclist:toggle_group_at(line)
        end,
        ["e"] = function(line)
            local location = loclist:get_location_at(line)
            if not location then
                return
            end
            vim.cmd("wincmd p")
            vim.cmd("e " .. location.filepath)
            vim.fn.cursor(location.lnum, location.col)
        end,
    },
    setup = function(ctx)
        async_update(ctx)
    end,
    update = function(ctx)
        async_update(ctx)
    end,
    toggle_all = function()
        loclist:toggle_all_groups()
    end,
    close_all = function()
        loclist:close_all_groups()
    end,
    open_all = function()
        loclist:open_all_groups()
    end,
    open = function(group)
        loclist:open_group(group)
    end,
    close = function(group)
        loclist:close_group(group)
    end,
    toggle = function(group)
        loclist:toggle_group(group)
    end,
}


================================================
FILE: lua/sidebar-nvim/colors.lua
================================================
local api = vim.api

local M = {}

local function get_hl_groups()
    return {}
end

local function get_links()
    return {
        SidebarNvimSectionTitle = "Directory",
        SidebarNvimSectionSeperator = "Comment",
        SidebarNvimSectionTitleSeperator = "Comment",
        SidebarNvimNormal = "Normal",
        SidebarNvimLabel = "Label",
        SidebarNvimComment = "Comment",
        SidebarNvimLineNr = "LineNr",
        SidebarNvimKeyword = "Keyword",
    }
end

function M.def_hl_group(group, gui, fg, bg)
    gui = gui and " gui=" .. gui or ""
    fg = fg and " guifg=" .. fg or ""
    bg = bg and " guibg=" .. bg or ""

    api.nvim_command("hi def " .. group .. gui .. fg .. bg)
end

function M.def_hl_link(group, link_to)
    api.nvim_command("hi def link " .. group .. " " .. link_to)
end

function M.setup()
    local higlight_groups = get_hl_groups()
    for k, d in pairs(higlight_groups) do
        M.def_hl_group(k, d.gui, d.fg, d.bg)
    end

    local links = get_links()
    for k, d in pairs(links) do
        M.def_hl_link(k, d)
    end
end

return M


================================================
FILE: lua/sidebar-nvim/components/basic.lua
================================================
local Component = {}

-- convert the current data structure into a list of lines + highlight groups
-- @param (table) ctx the draw context
-- @param (table) list of lines (strings)
-- @param (table) list of hl groups
-- luacheck: push ignore
function Component:draw(ctx, section_lines, section_hl) end
-- luacheck: pop

return Component


================================================
FILE: lua/sidebar-nvim/components/loclist.lua
================================================
local Component = require("sidebar-nvim.components.basic")

local Loclist = {}

Loclist.DEFAULT_OPTIONS = {
    groups = {},
    group_icon = { closed = "", opened = "" },
    -- badge showing the number of items in each group
    show_group_count = true,
    -- if empty groups should be displayed
    show_empty_groups = true,
    -- if there's a single group, skip rendering the group controls
    omit_single_group = false,
    -- initial state of the groups
    groups_initially_closed = false,
    -- highlight groups for each control element
    highlights = {
        group = "SidebarNvimLabel",
        group_count = "SidebarNvimSectionTitle",
    },
}

setmetatable(Loclist, { __index = Component })

-- creates a new loclist component
-- @param (table) o
-- |- (table) o.groups list of groups containing (table) items. See Loclist:add_item
-- |- (boolean) o.show_group_count show a badge after the group name with the count of items contained in the group
-- |- (boolean) o.omit_single_group whether this component should draw the group line if there's only one group present
function Loclist:new(o)
    o = vim.tbl_deep_extend("force", vim.deepcopy(Loclist.DEFAULT_OPTIONS), o or {}, {
        -- table(line_number -> group ref)
        _group_indexes = {},
        -- table(line__number -> item ref)
        _location_indexes = {},
        -- used to keep the group list stable
        _group_keys = {},
    })

    o._group_keys = vim.tbl_keys(o.groups or {})

    setmetatable(o, self)
    self.__index = self
    return o
end

-- adds a new item to the loclist
-- @param (table) item
-- |- (string) item.group the group name that this item will live
-- |- (number) item.lnum the line number of this item
-- |- (number) item.col the col number of this item
-- |- (table|array) item.left
-- |--|- (string) item.left[n].text = "abc"
-- |--|- (string) item.left[n].hl = "<highlight group>"
-- |- (table|array) item.right
-- |--|- (string) item.left[n].text = "abc"
-- |--|- (string) item.left[n].hl = "<highlight group>"
-- |- (number) item.order items are sorted based on order within each group
function Loclist:add_item(item)
    if not self.groups[item.group] then
        self.groups[item.group] = { is_closed = self.groups_initially_closed or false }
    end

    if not vim.tbl_contains(self._group_keys, item.group) then
        table.insert(self._group_keys, item.group)
    end

    local group_tbl = self.groups[item.group]
    group_tbl[#group_tbl + 1] = item

    if item.order then
        table.sort(self.groups[item.group], function(a, b)
            return a.order < b.order
        end)
    else
        item.order = 0
    end
end

-- replace all the items with the new list
-- @param (table) list of items
-- |- items[...]
-- |-- (string) item.group the group name that this item will live
-- |-- (number) item.lnum the line number of this item
-- |-- (number) item.col the col number of this item
-- |- (table|array) item.left
-- |--|- (string) item.left[n].text = "abc"
-- |--|- (string) item.left[n].hl = "<highlight group>"
-- |- (table|array) item.right
-- |--|- (string) item.left[n].text = "abc"
-- |--|- (string) item.left[n].hl = "<highlight group>"
-- |- clear_opts (table) see Loclist:clear
function Loclist:set_items(items, clear_opts)
    self:clear(clear_opts)

    for _, item in ipairs(items) do
        self:add_item(item)
    end

    if clear_opts and clear_opts.remove_groups then
        self._group_keys = vim.tbl_keys(self.groups)
    end
end

-- add an empty group
-- @param group string: name of the group
function Loclist:add_group(group)
    if not self.groups[group] then
        self.groups[group] = { is_closed = true }
        self._group_keys[#self._group_keys + 1] = group
    end
end

-- clear all the groups
-- @param opts (table)
-- |- opts.remove_groups (boolean) also remove groups from the list, otherwise only items will be removed, removing groups from the list also means that the state of groups will be cleared
function Loclist:clear(opts)
    opts = opts or {}

    if opts.remove_groups then
        self.groups = {}
        self._group_keys = {}
        return
    end

    for _, key in ipairs(self._group_keys) do
        self.groups[key] = { is_closed = self.groups[key].is_closed }
    end
end

function Loclist:draw_group(ctx, group_name, with_label, section_lines, section_hl)
    local group = self.groups[group_name]

    if #group == 0 and not self.show_empty_groups then
        return
    end

    if with_label then
        local icon = self.group_icon.opened
        if #group == 0 or group.is_closed then
            icon = self.group_icon.closed
        end

        local group_title = icon .. " " .. group_name

        local line = group_title

        if line:len() > ctx.width - 1 then
            line = line:sub(1, ctx.width - 5) .. "..."
        end

        table.insert(section_hl, { self.highlights.group, #section_lines, 0, #line })
        if self.show_group_count then
            table.insert(section_hl, { self.highlights.group_count, #section_lines, #line, -1 })
            local total = #group
            if total > 99 then
                total = "++"
            end
            line = line .. " (" .. total .. ")"
        end

        self._group_indexes[#section_lines] = group
        table.insert(section_lines, line)
    end

    if group.is_closed then
        return
    end

    for _, item in ipairs(group) do
        self._location_indexes[#section_lines] = item
        local line = ""

        if with_label then
            line = "  "
        end

        if type(item.left) == "table" and #item.left ~= 0 then
            for _, i in ipairs(item.left) do
                if i ~= nil and i.text ~= nil then
                    -- Calculate space left in line
                    local space_left = ctx.width - #line

                    -- Break if line is already full
                    if space_left <= 0 then
                        break
                    end

                    if i.hl then
                        table.insert(section_hl, { i.hl, #section_lines, #line, -1 })
                    else
                        table.insert(section_hl, { "SidebarNvimNormal", #section_lines, #line, -1 })
                    end
                    line = line .. tostring(i.text):sub(1, space_left)
                end
            end
        end

        if type(item.right) == "table" and #item.right ~= 0 then
            local temp_line = ""
            local temp_hl = {}

            for _, i in ipairs(item.right) do
                if i ~= nil and i.text ~= nil then
                    -- Calculate space left in line
                    local space_left = ctx.width - #line - #temp_line - 1

                    -- Break if line is already full
                    if space_left <= 0 then
                        break
                    end

                    if i.hl then
                        table.insert(temp_hl, { i.hl, #section_lines, #line + #temp_line, -1 })
                    else
                        table.insert(temp_hl, { "SidebarNvimNormal", #section_lines, #line + #temp_line, -1 })
                    end

                    temp_line = temp_line .. i.text:sub(1, space_left)
                end
            end

            -- Calculate offset and add empty space in the middle
            local offset = ctx.width - #temp_line - #line
            local gap = string.rep(" ", offset)
            line = line .. gap .. temp_line

            -- Add highlights accounting for offset
            for _, hl in ipairs(temp_hl) do
                table.insert(section_hl, { hl[1], hl[2], hl[3] + offset, hl[4] })
            end
        end

        table.insert(section_lines, line)
    end
end

-- convert the current data structure into a list of lines + highlight groups
-- @return (table) list of lines (strings)
-- @return (table) list of hl groups
function Loclist:draw(ctx, section_lines, section_hl)
    self._group_indexes = {}
    self._location_indexes = {}

    if #self._group_keys == 1 and self.omit_single_group then
        self:draw_group(ctx, self._group_keys[1], false, section_lines, section_hl)
        return
    end

    for _, group_name in ipairs(self._group_keys) do
        self:draw_group(ctx, group_name, true, section_lines, section_hl)
    end
end

-- returns the location specified in the location printed on line `line`
-- if the line does not have a location rendered, return nil
-- @param (number) line
function Loclist:get_location_at(line)
    local location = self._location_indexes[line]
    return location
end

-- toggles the group open/close that is printed on line `line`
-- if there is no group at `line`, then do nothing
-- @param (number) line
function Loclist:toggle_group_at(line)
    local group = self._group_indexes[line]
    if not group then
        return
    end

    group.is_closed = not group.is_closed
end

-- Toggle group with name `group_name`
-- @param group_name string: the name of group to toggle
function Loclist:toggle_group(group_name)
    local group = self.groups[group_name]
    if not group then
        return
    end

    group.is_closed = not group.is_closed
end

-- Open group with name `group_name`
-- @param group_name string: the name of group to open
function Loclist:open_group(group_name)
    local group = self.groups[group_name]
    if not group then
        return
    end

    group.is_closed = false
end

-- Close group with name `group_name`
-- @param group_name string: the name of group to close
function Loclist:close_group(group_name)
    local group = self.groups[group_name]
    if not group then
        return
    end

    group.is_closed = true
end

-- toggle all groups
function Loclist:toggle_all_groups()
    for _, group in pairs(self.groups) do
        group.is_closed = not group.is_closed
    end
end

-- opens all groups
function Loclist:open_all_groups()
    for _, group in pairs(self.groups) do
        group.is_closed = false
    end
end

-- closes all groups
function Loclist:close_all_groups()
    for _, group in pairs(self.groups) do
        group.is_closed = true
    end
end

return Loclist


================================================
FILE: lua/sidebar-nvim/config.lua
================================================
local M = {}

M.disable_default_keybindings = 0
M.bindings = nil
M.side = "left"
M.initial_width = 35

M.hide_statusline = false

M.update_interval = 1000

M.enable_profile = false

M.sections = { "datetime", "git", "diagnostics" }

M.section_separator = { "", "-----", "" }

M.section_title_separator = { "" }

M.git = { icon = "" }

M.diagnostics = { icon = "" }

M.buffers = {
    icon = "",
    ignored_buffers = {},
    sorting = "id",
    show_numbers = true,
    ignore_not_loaded = false,
    ignore_terminal = true,
}

M.symbols = { icon = "ƒ" }

M.containers = { icon = "", use_podman = false, attach_shell = "/bin/sh", show_all = true, interval = 5000 }

M.datetime = { icon = "", format = "%a %b %d, %H:%M", clocks = { { name = "local" } } }

M.todos = { icon = "", ignored_paths = { "~" }, initially_closed = false }

M.files = { icon = "", show_hidden = false, ignored_paths = { "%.git$" } }

return M


================================================
FILE: lua/sidebar-nvim/debouncer.lua
================================================
local luv = vim.loop

local Debouncer = {}

function Debouncer:new(fn, delay)
    local o = { fn = fn, delay = delay, locked = false }

    setmetatable(o, self)
    self.__index = self

    return o
end

function Debouncer:start_timer()
    if self.timer then
        self.timer:stop()
        self.timer:close()
        self.timer = nil
    end

    self.timer = luv.new_timer()
    self.timer:start(
        self.delay,
        0,
        vim.schedule_wrap(function()
            self.locked = false
            self.timer:stop()
            self.timer:close()
            self.timer = nil
        end)
    )
end

function Debouncer:call(...)
    local args = { ... }
    vim.schedule(function()
        if self.locked then
            return
        end

        self.locked = true
        self.fn(unpack(args))
        self:start_timer()
    end)
end

return Debouncer


================================================
FILE: lua/sidebar-nvim/docker_utils.lua
================================================
local config = require("sidebar-nvim.config")
local luv = vim.loop

local M = {}

function M.get_docker_bin()
    local bin = "docker"

    if config.containers.use_podman then
        bin = "podman"
    end

    return bin
end

function M.build_docker_command(args, stdout, stderr)
    local bin = M.get_docker_bin()

    args = args or {}
    -- make sure the command only fetches the bare minimum fields to work
    -- otherwise docker goes crazy with high load. See https://github.com/sidebar-nvim/sidebar.nvim/issues/3
    -- we also need to make sure that each line has valid json syntax so the parser can understand
    -- the formatting string below is to make sure we reconstruct the json object with only the fields we want
    table.insert(args, '--format=\'{"Names": {{json .Names}}, "State": {{json .State}}, "ID": {{json .ID}} }\'')

    return { bin = bin, opts = { args = args, stdio = { nil, stdout, stderr }, cwd = luv.cwd() } }
end

function M.build_docker_attach_command(container_id)
    local bin = M.get_docker_bin()

    return bin .. " exec -it " .. container_id .. " " .. config.containers.attach_shell
end

return M


================================================
FILE: lua/sidebar-nvim/events.lua
================================================
local M = {}

local global_handlers = {}

local Event = { Ready = "Ready" }

local function get_handlers(event_name)
    return global_handlers[event_name] or {}
end

local function register_handler(event_name, handler)
    local handlers = get_handlers(event_name)
    table.insert(handlers, handler)
    global_handlers[event_name] = handlers
end

local function dispatch(event_name, payload)
    for _, handler in pairs(get_handlers(event_name)) do
        local success, error = pcall(handler, payload)
        if not success then
            vim.api.nvim_err_writeln("Handler for event " .. event_name .. " errored. " .. vim.inspect(error))
        end
    end
end

-- @private
function M._dispatch_ready()
    dispatch(Event.Ready)
end

-- Registers a handler for the Ready event.
-- @param handler (function) Handler with the signature `function()`
function M.on_sidebar_nvim_ready(handler)
    register_handler(Event.Ready, handler)
end

return M


================================================
FILE: lua/sidebar-nvim/lib.lua
================================================
local luv = vim.loop
local api = vim.api

local renderer = require("sidebar-nvim.renderer")
local view = require("sidebar-nvim.view")
local events = require("sidebar-nvim.events")
local updater = require("sidebar-nvim.updater")
local config = require("sidebar-nvim.config")
local bindings = require("sidebar-nvim.bindings")
local utils = require("sidebar-nvim.utils")

local first_init_done = false

local M = {}

M.State = { section_line_indexes = {} }

M.timer = nil

local function _redraw()
    if vim.v.exiting ~= vim.NIL then
        return
    end

    M.State.section_line_indexes = renderer.draw(updater.sections_data)
end

local function loop()
    if not view.is_win_open({ any_tabpage = true }) then
        return
    end

    updater.draw()
    _redraw()
end

local function _start_timer(should_delay)
    M.timer = luv.new_timer()

    local delay = 100
    if should_delay then
        delay = config.update_interval
    end

    -- wait `delay`ms and then repeats every `config.update_interval`ms
    M.timer:start(
        delay,
        config.update_interval,
        vim.schedule_wrap(function()
            loop()
        end)
    )
end

function M.setup()
    _redraw()

    _start_timer(false)

    if not first_init_done then
        events._dispatch_ready()
        first_init_done = true
    end
end

function M.update()
    if M.timer ~= nil then
        M.timer:stop()
        M.timer:close()
        M.timer = nil
    end

    if view.is_win_open({ any_tabpage = true }) then
        updater.update()
    end
    loop()

    _start_timer(true)
end

function M.open(opts)
    view.open(opts or { focus = false })
    M.update()
end

function M.close()
    if view.is_win_open() then
        view.close()
    end
end

function M.toggle(opts)
    if view.is_win_open() then
        M.close()
        return
    end

    M.open(opts)
end

-- Resize the sidebar to the requested size
-- @param size number
function M.resize(size)
    view.View.width = size
    view.resize()
end

-- @param opts table
-- @param |- opts.any_tabpage boolean if true check if is open in any tabpage, if false check in current tab
function M.is_open(opts)
    return view.is_win_open(opts)
end
--
-- Focus or open the sidebar
-- @param opts table
-- @param opts.section_index number
-- @param opts.cursor_at_content boolean
function M.focus(opts)
    if view.is_win_open() then
        local winnr = view.get_winnr()
        view.focus(winnr)
    else
        M.open({ focus = true })
    end

    if opts and opts.section_index then
        local content_only = true

        if opts.cursor_at_content == false then
            content_only = false
        end

        local cursor = M.find_cursor_at_section_index(opts.section_index, { content_only = content_only })

        if cursor then
            api.nvim_win_set_cursor(0, cursor)
        end
    end
end

--- Returns the window width for sidebar-nvim within the tabpage specified
---@param tabpage number: (optional) the number of the chosen tabpage. Defaults to current tabpage.
---@return number
function M.get_width(tabpage)
    return view.get_width(tabpage)
end

function M.destroy()
    view.close()

    if M.timer ~= nil then
        M.timer:stop()
        M.timer:close()
        M.timer = nil
    end

    view._wipe_rogue_buffer()
end

local function get_start_line(content_only, indexes)
    if content_only then
        return indexes.content_start
    end

    return indexes.section_start
end

local function get_end_line(content_only, indexes)
    if content_only then
        return indexes.content_start + indexes.content_length
    end

    return indexes.section_start + indexes.section_length - 1
end

-- @param opts: table
-- @param opts.content_only: boolean = whether the it should only check if the cursor is hovering the contents of the section
-- @return table{section_index = number, section_content_current_line = number, cursor_col = number, cursor_line = number)
function M.find_section_at_cursor(opts)
    opts = opts or { content_only = true }

    local cursor = opts.cursor or api.nvim_win_get_cursor(0)
    local cursor_line = cursor[1]
    local cursor_col = cursor[2]

    for section_index, section_line_index in ipairs(M.State.section_line_indexes) do
        local start_line = get_start_line(opts.content_only, section_line_index)
        local end_line = get_end_line(opts.content_only, section_line_index)
        -- check if the start of this section is after the cursor line

        if cursor_line >= start_line and cursor_line <= end_line then
            return {
                section_index = section_index,
                section_content_current_line = cursor_line - section_line_index.content_start,
                cursor_line = cursor_line,
                cursor_col = cursor_col,
                line_index = section_line_index,
            }
        end
    end

    return nil
end

-- this is the oposite of `find_section_at_cursor`, given a section index, find the current line in the buffer
-- @param index number
-- @param opts table
-- @param |- opts.content_only boolean whether the cursor should be placed at the first line of content or the section title
-- @return table with cursor {line: number, col: number}
-- @return nil
function M.find_cursor_at_section_index(index, opts)
    opts = opts or { content_only = false }

    local cursor = { 0, 0 }

    for section_index, section_line_index in ipairs(M.State.section_line_indexes) do
        if section_index == index then
            local start_line = get_start_line(opts.content_only, section_line_index)

            cursor[1] = start_line
            return cursor
        end
    end

    return nil
end

function M.on_keypress(key)
    local section_match = M.find_section_at_cursor()
    bindings.on_keypress(utils.unescape_keycode(key), section_match)
    M.update()
end

function M.on_cursor_move(direction)
    local cursor = api.nvim_win_get_cursor(0)
    local line = cursor[1]

    local current_section = M.find_section_at_cursor({ content_only = false })

    if not current_section then
        current_section = M.find_section_at_cursor({ content_only = false, cursor = { 1, 1 } })
    end

    local current_section_index = current_section.section_index
    local current_line_index = current_section.line_index

    local next_line = line + 1
    if direction == "up" then
        next_line = line - 1
    end

    if direction == "down" then
        if next_line > current_line_index.section_start and next_line < current_line_index.content_start then
            next_line = current_line_index.content_start
        elseif next_line >= current_line_index.content_start + current_line_index.content_length then
            local next_section = M.State.section_line_indexes[current_section_index + 1]
            if not next_section then
                return
            end
            next_line = next_section.section_start
        end
    else
        if next_line < current_line_index.section_start then
            local next_section = M.State.section_line_indexes[current_section_index - 1]
            if not next_section then
                return
            end
            next_line = next_section.content_start + next_section.content_length - 1
        elseif next_line < current_line_index.content_start and next_line > current_line_index.section_start then
            next_line = current_line_index.section_start
        end
    end

    api.nvim_win_set_cursor(0, { next_line, 1 })
end

function M.on_tab_change()
    vim.schedule(function()
        if not view.is_win_open() and view.is_win_open({ any_tabpage = true }) then
            view.open({ focus = false })
        end
    end)
end

function M.on_win_leave()
    vim.defer_fn(function()
        if not view.is_win_open() then
            return
        end

        local windows = api.nvim_list_wins()
        local curtab = api.nvim_get_current_tabpage()
        local wins_in_tabpage = vim.tbl_filter(function(w)
            return api.nvim_win_get_tabpage(w) == curtab
        end, windows)
        if #windows == 1 then
            M.close()
        elseif #wins_in_tabpage == 1 then
            api.nvim_command(":tabclose")
        end
    end, 50)
end

function M.on_vim_leave()
    M.destroy()
end

return M


================================================
FILE: lua/sidebar-nvim/profile.lua
================================================
local config = require("sidebar-nvim.config")

local M = {}

M.entries = {}

function M.clear()
    M.entries = {}
end

function M.add_point(name, value, unit)
    M.entries[name] = { value = value, unit = unit }
end

function M.run(name, fn, ...)
    if not config.enable_profile then
        return fn(...)
    end

    local time_before = vim.loop.hrtime()

    local ret = { fn(...) }

    local duration = vim.loop.hrtime() - time_before

    M.add_point(name, duration, "ns")

    return unpack(ret)
end

function M.wrap_fn(name, fn)
    return function(...)
        return M.run(name, fn, ...)
    end
end

function M.print_summary(filter)
    local filtered_keys = vim.tbl_keys(M.entries)

    if filter then
        filtered_keys = vim.tbl_filter(function(entry)
            return vim.tbl_contains(filter, entry)
        end, vim.tbl_keys(M.entries))
    end

    local entries = {}
    for _, key in ipairs(filtered_keys) do
        table.insert(entries, vim.tbl_deep_extend("force", { name = key }, M.entries[key]))
    end

    -- TODO: convert units

    table.sort(entries, function(a, b)
        -- reverse sort
        return a.value > b.value
    end)

    for _, entry in ipairs(entries) do
        print(string.format("Name: %s Value: %f", entry.name, entry.value / 1000000))
    end
end

return M


================================================
FILE: lua/sidebar-nvim/renderer.lua
================================================
local view = require("sidebar-nvim.view")
local config = require("sidebar-nvim.config")
local utils = require("sidebar-nvim.utils")
local profile = require("sidebar-nvim.profile")

local api = vim.api

local namespace_id = api.nvim_create_namespace("SidebarNvimHighlights")

local M = {}

-- extracted from this PR: https://github.com/sidebar-nvim/sidebar.nvim/pull/41
-- thanks @lambdahands
local function sanitize_lines(lines)
    local lines_ = {}
    for _, line_ in ipairs(lines) do
        local line = string.gsub(line_, "[\n\r]", " ")
        table.insert(lines_, line)
    end
    return lines_
end

local function expand_section_lines(section_lines, lines_offset)
    if type(section_lines) == "string" then
        return vim.split(section_lines, "\n"), nil
    elseif type(section_lines) == "table" and section_lines.lines == nil then
        return sanitize_lines(section_lines), nil
    end

    -- we have here section_lines = { lines = string|table of strings, hl = table }

    local section_hl = section_lines.hl or {}
    section_lines = section_lines.lines

    if type(section_lines) == "string" then
        section_lines = vim.split(section_lines, "\n")
    else
        section_lines = sanitize_lines(section_lines)
    end

    -- we must offset the hl lines so it matches the current section position
    for _, hl_entry in ipairs(section_hl) do
        hl_entry[2] = hl_entry[2] + lines_offset
    end

    return section_lines, section_hl
end

local function build_section_title(section)
    local icon = "#"
    if section.icon ~= nil then
        icon = section.icon
    end

    if type(icon) == "function" then
        icon = icon()
    end

    return icon .. " " .. section.title
end

local function build_section_separator(section, index)
    if type(config.section_separator) == "string" then
        return { config.section_separator }
    end

    if type(config.section_separator) == "table" then
        return config.section_separator
    end

    if type(config.section_separator) ~= "function" then
        utils.echo_warning("'section_separator' must be string, table or function")
        return
    end

    return config.section_separator(section, index)
end

local function build_section_title_separator(section, index)
    if type(config.section_title_separator) == "string" then
        return { config.section_title_separator }
    end

    if type(config.section_title_separator) == "table" then
        return config.section_title_separator
    end

    if type(config.section_title_separator) ~= "function" then
        utils.echo_warning("'section_title_separator' must be false, string, table or function")
        return
    end

    return config.section_title_separator(section, index)
end

local function get_lines_and_hl(sections_data)
    local lines = {}
    local hl = {}
    local section_line_indexes = {}

    for index, data in ipairs(sections_data) do
        local section_title = build_section_title(data.section)
        local section_title_length = 1

        table.insert(hl, { "SidebarNvimSectionTitle", #lines, 0, #section_title })

        local section_title_separator = build_section_title_separator(data.section, index)

        local section_content_start = #lines + section_title_length + #section_title_separator + 1
        local section_title_start = #lines + section_title_length
        table.insert(lines, section_title)

        for _, line in ipairs(section_title_separator) do
            table.insert(hl, { "SidebarNvimSectionTitleSeperator", #lines, 0, #line })
            table.insert(lines, line)
        end

        local section_lines, section_hl = expand_section_lines(data.lines, #lines)

        table.insert(section_line_indexes, {
            content_start = section_content_start,
            content_length = #section_lines,
            section_start = section_title_start,
            section_length = #section_lines + section_title_length + #section_title_separator + 1,
        })

        for _, line in ipairs(section_lines) do
            table.insert(lines, line)
        end

        for _, hl_entry in ipairs(section_hl or {}) do
            table.insert(hl, hl_entry)
        end

        if index ~= #sections_data then
            local separator = build_section_separator(data.section, index)

            for _, line in ipairs(separator) do
                table.insert(hl, { "SidebarNvimSectionSeperator", #lines, 0, #line })
                table.insert(lines, line)
            end
        end
    end

    return lines, hl, section_line_indexes
end

function M.draw(sections_data)
    return profile.run("view.render", function()
        if not api.nvim_buf_is_loaded(view.View.bufnr) then
            return
        end

        local cursor
        if view.is_win_open() then
            cursor = api.nvim_win_get_cursor(view.get_winnr())
        end

        local lines, hl, section_line_indexes = get_lines_and_hl(sections_data)

        api.nvim_buf_set_option(view.View.bufnr, "modifiable", true)
        api.nvim_buf_set_lines(view.View.bufnr, 0, -1, false, lines)
        M.render_hl(view.View.bufnr, hl)
        api.nvim_buf_set_option(view.View.bufnr, "modifiable", false)

        if view.is_win_open() then
            if cursor and #lines >= cursor[1] then
                api.nvim_win_set_cursor(view.get_winnr(), cursor)
            end
            if cursor then
                api.nvim_win_set_option(view.get_winnr(), "wrap", false)
            end

            if config.hide_statusline then
                api.nvim_win_set_option(view.get_winnr(), "statusline", "%#NonText#")
            end
        end

        return section_line_indexes
    end)
end

function M.render_hl(bufnr, hl)
    if not api.nvim_buf_is_loaded(bufnr) then
        return
    end
    api.nvim_buf_clear_namespace(bufnr, namespace_id, 0, -1)
    for _, data in ipairs(hl) do
        api.nvim_buf_add_highlight(bufnr, namespace_id, data[1], data[2], data[3], data[4])
    end
end

return M


================================================
FILE: lua/sidebar-nvim/updater.lua
================================================
local utils = require("sidebar-nvim.utils")
local view = require("sidebar-nvim.view")
local config = require("sidebar-nvim.config")
local profile = require("sidebar-nvim.profile")
local colors = require("sidebar-nvim.colors")

local M = {}

-- list of sections rendered
-- { { lines = lines..., section = <table> }, { lines =  lines..., section = <table> } }
M.sections_data = {}

function M.setup()
    if config.sections == nil then
        return
    end

    local ctx = { width = view.get_width() }

    for section_index, section_data in ipairs(config.sections) do
        local section = utils.resolve_section(section_index, section_data)
        if section then
            local hl_def = section.highlights or {}

            for hl_group, hl_group_data in pairs(hl_def.groups or {}) do
                colors.def_hl_group(hl_group, hl_group_data.gui, hl_group_data.fg, hl_group_data.bg)
            end

            for hl_group, hl_group_link_to in pairs(hl_def.links or {}) do
                colors.def_hl_link(hl_group, hl_group_link_to)
            end

            if section.setup then
                section.setup(ctx)
            end
        end
    end
end

function M.update()
    return profile.run("update.sections.total", function()
        if vim.v.exiting ~= vim.NIL then
            return
        end

        local ctx = { width = view.View.width }

        for section_index, section_data in pairs(config.sections) do
            local section = utils.resolve_section(section_index, section_data)

            if section ~= nil and section.update ~= nil then
                profile.run("update.sections." .. section_index, section.update, ctx)
            end
        end
    end)
end

function M.draw()
    return profile.run("draw.sections.total", function()
        if vim.v.exiting ~= vim.NIL then
            return
        end

        M.sections_data = {}

        local draw_ctx = { width = view.View.width }

        for section_index, section_data in pairs(config.sections) do
            local section = utils.resolve_section(section_index, section_data)

            if section ~= nil then
                local section_lines = profile.run("draw.sections." .. section_index, section.draw, draw_ctx)
                local data = { lines = section_lines, section = section }
                table.insert(M.sections_data, data)
            end
        end
    end)
end

return M


================================================
FILE: lua/sidebar-nvim/utils.lua
================================================
local M = {}
local api = vim.api
local luv = vim.loop

function M.echo_warning(msg)
    api.nvim_command("echohl WarningMsg")
    api.nvim_command("echom '[SidebarNvim] " .. msg:gsub("'", "''") .. "'")
    api.nvim_command("echohl None")
end

function M.escape_keycode(key)
    return key:gsub("<", "["):gsub(">", "]")
end

function M.unescape_keycode(key)
    return key:gsub("%[", "<"):gsub("%]", ">")
end

function M.sidebar_nvim_callback(key)
    return string.format(":lua require('sidebar-nvim.lib').on_keypress('%s')<CR>", M.escape_keycode(key))
end

function M.sidebar_nvim_cursor_move_callback(direction)
    return string.format(":lua require('sidebar-nvim')._on_cursor_move('%s')<CR>", direction)
end

local function get_builtin_section(name)
    local ret, section = pcall(require, "sidebar-nvim.builtin." .. name)
    if not ret then
        M.echo_warning("error trying to load section: " .. name)
        return nil
    end

    return section
end

function M.resolve_section(index, section)
    if type(section) == "string" then
        return get_builtin_section(section)
    elseif type(section) == "table" then
        return section
    end

    M.echo_warning("invalid SidebarNvim section at: index=" .. index .. " section=" .. section)
    return nil
end

function M.is_instance(o, class)
    while o do
        o = getmetatable(o)
        if class == o then
            return true
        end
    end
    return false
end

-- Reference: https://github.com/hoob3rt/lualine.nvim/blob/master/lua/lualine/components/filename.lua#L9

local function count(base, pattern)
    return select(2, string.gsub(base, pattern, ""))
end

function M.shorten_path(path, min_len)
    if #path <= min_len then
        return path
    end

    local sep = package.config:sub(1, 1)

    for _ = 0, count(path, sep) do
        if #path <= min_len then
            return path
        end

        -- ('([^/])[^/]+%/', '%1/', 1)
        path = path:gsub(string.format("([^%s])[^%s]+%%%s", sep, sep, sep), "%1" .. sep, 1)
    end

    return path
end

function M.shortest_path(path)
    local sep = package.config:sub(1, 1)

    for _ = 0, count(path, sep) do
        -- ('([^/])[^/]+%/', '%1/', 1)
        path = path:gsub(string.format("([^%s])[^%s]+%%%s", sep, sep, sep), "%1" .. sep, 1)
    end

    return path
end

function M.dir(path)
    return path:match("^(.+/)")
end

function M.filename(path)
    local split = vim.split(path, "/")
    return split[#split]
end

function M.file_exist(path)
    local _, err = luv.fs_stat(path)
    return err == nil
end

function M.truncate(s, size)
    local length = #s

    if length <= size then
        return s
    else
        return s:sub(1, size) .. ".."
    end
end

function M.async_cmd(cmd, args, callback)
    local stdout = luv.new_pipe(false)
    local stderr = luv.new_pipe(false)
    local handle

    handle = luv.spawn(cmd, { args = args, stdio = { nil, stdout, stderr }, cwd = luv.cwd() }, function()
        if callback then
            callback()
        end

        luv.read_stop(stdout)
        luv.read_stop(stderr)
        stdout:close()
        stderr:close()
        handle:close()
    end)

    luv.read_start(stdout, function(err, _)
        if err ~= nil then
            vim.schedule(function()
                M.echo_warning(err)
            end)
        end
    end)

    luv.read_start(stderr, function(err, data)
        if data ~= nil then
            vim.schedule(function()
                M.echo_warning(data)
            end)
        end

        if err ~= nil then
            vim.schedule(function()
                M.echo_warning(err)
            end)
        end
    end)
end

-- @param opts table
-- @param opts.modified boolean filter buffers by modified or not
function M.get_existing_buffers(opts)
    return vim.tbl_filter(function(buf)
        local modified_filter = true
        if opts and opts.modified ~= nil then
            local is_ok, is_modified = pcall(api.nvim_buf_get_option, buf, "modified")

            if is_ok then
                modified_filter = is_modified == opts.modified
            end
        end

        return api.nvim_buf_is_valid(buf) and vim.fn.buflisted(buf) == 1 and modified_filter
    end, api.nvim_list_bufs())
end

return M


================================================
FILE: lua/sidebar-nvim/view.lua
================================================
local bindings = require("sidebar-nvim.bindings")
local config = require("sidebar-nvim.config")
local utils = require("sidebar-nvim.utils")

local a = vim.api

local M = {}

M.is_prompt_exiting = false

M.View = {
    bufnr = nil,
    tabpages = {},
    width = 30,
    side = "left",
    winopts = {
        relativenumber = false,
        number = false,
        list = false,
        winfixwidth = true,
        winfixheight = true,
        foldenable = false,
        spell = false,
        signcolumn = "yes",
        foldmethod = "manual",
        foldcolumn = "0",
        cursorcolumn = false,
        colorcolumn = "0",
    },
    bufopts = {
        { name = "swapfile", val = false },
        { name = "buftype", val = "nofile" },
        { name = "modifiable", val = false },
        { name = "filetype", val = "SidebarNvim" },
        { name = "bufhidden", val = "hide" },
    },
}

---Find a rogue SidebarNvim buffer that might have been spawned by i.e. a session.
---@return integer|nil
local function find_rogue_buffer()
    for _, v in ipairs(a.nvim_list_bufs()) do
        if string.match(vim.fn.bufname(v), "^SidebarNvim_.*") then
            return v
        end
    end
    return nil
end

---Check if the tree buffer is valid and loaded.
---@return boolean
local function is_buf_valid()
    if M.View.bufnr == nil then
        return false
    end
    return a.nvim_buf_is_valid(M.View.bufnr) and a.nvim_buf_is_loaded(M.View.bufnr)
end

---Find pre-existing SidebarNvim buffer, delete its windows then wipe it.
---@private
function M._wipe_rogue_buffer()
    local bn = find_rogue_buffer()
    if bn then
        local win_ids = vim.fn.win_findbuf(bn)
        for _, id in ipairs(win_ids) do
            if vim.fn.win_gettype(id) ~= "autocmd" then
                a.nvim_win_close(id, true)
            end
        end

        a.nvim_buf_set_name(bn, "")
        vim.schedule(function()
            pcall(a.nvim_buf_delete, bn, {})
        end)
    end
end

local function generate_buffer_name()
    return "SidebarNvim_" .. math.random(1000000)
end

-- set user options and create tree buffer (should never be wiped)
function M.setup()
    M.View.side = config.side or M.View.side
    M.View.width = config.initial_width or M.View.width

    M.View.bufnr = a.nvim_create_buf(false, false)
    bindings.inject(M.View.bufnr)

    local buffer_name = generate_buffer_name()

    if not pcall(a.nvim_buf_set_name, M.View.bufnr, buffer_name) then
        M._wipe_rogue_buffer()
        a.nvim_buf_set_name(M.View.bufnr, buffer_name)
    end

    for _, opt in ipairs(M.View.bufopts) do
        vim.bo[M.View.bufnr][opt.name] = opt.val
    end

    vim.api.nvim_exec(
        [[
augroup sidebar_nvim_prevent_buffer_override
    autocmd!
    autocmd BufWinEnter * lua require('sidebar-nvim.view')._prevent_buffer_override()
augroup END
]],
        false
    )
end

local goto_tbl = { right = "h", left = "l", top = "j", bottom = "k" }

function M._prevent_buffer_override()
    vim.schedule(function()
        local curwin = a.nvim_get_current_win()
        local curbuf = a.nvim_win_get_buf(curwin)
        if curwin ~= M.get_winnr() or curbuf == M.View.bufnr then
            return
        end

        vim.cmd("buffer " .. M.View.bufnr)

        if #vim.api.nvim_list_wins() < 2 then
            vim.cmd("vsplit")
        else
            vim.cmd("wincmd " .. goto_tbl[M.View.side])
        end

        -- copy target window options
        local winopts_target = vim.deepcopy(M.View.winopts)
        for key, _ in pairs(winopts_target) do
            winopts_target[key] = a.nvim_win_get_option(0, key)
        end

        -- change the buffer will override the target window with the sidebar window opts
        vim.cmd("buffer " .. curbuf)

        -- revert the changes made when changing buffer
        for key, value in pairs(winopts_target) do
            a.nvim_win_set_option(0, key, value)
        end

        M.resize()
    end)
end

function M.win_open(opts)
    -- TODO: [deprecated] to remove
    utils.echo_warning("view.win_open() is now deprecated, please use 'require('sidebar-nvim').is_open()'")
    return M.is_win_open(opts)
end

-- @param opts table
-- @param |- opts.any_tabpage boolean if true check if is open in any tabpage, if false check in current tab
function M.is_win_open(opts)
    if opts and opts.any_tabpage then
        for _, v in pairs(M.View.tabpages) do
            if a.nvim_win_is_valid(v.winnr) then
                return true
            end
        end
        return false
    else
        return M.get_winnr() ~= nil and a.nvim_win_is_valid(M.get_winnr())
    end
end

function M.set_cursor(opts)
    if M.is_win_open() then
        pcall(a.nvim_win_set_cursor, M.get_winnr(), opts)
    end
end

function M.focus(winnr)
    local wnr = winnr or M.get_winnr()

    if wnr == nil then
        return
    end

    if a.nvim_win_get_tabpage(wnr) ~= a.nvim_win_get_tabpage(0) then
        M.close()
        M.open()
        wnr = M.get_winnr()
    end

    a.nvim_set_current_win(wnr)
end

local function get_defined_width()
    if type(M.View.width) == "number" then
        return M.View.width
    end
    local width_as_number = tonumber(M.View.width:sub(0, -2))
    local percent_as_decimal = width_as_number / 100
    return math.floor(vim.o.columns * percent_as_decimal)
end

function M.resize()
    if not M.is_win_open() then
        return
    end

    if not a.nvim_win_is_valid(M.get_winnr()) then
        return
    end

    a.nvim_win_set_width(M.get_winnr(), get_defined_width())
end

local move_tbl = { left = "H", right = "L", bottom = "J", top = "K" }

local function set_local(opt, value)
    a.nvim_win_set_option(0, opt, value)
end

function M.open(options)
    options = options or { focus = false }
    if not is_buf_valid() then
        M.setup()
    end

    a.nvim_command("vsp")

    local move_to = move_tbl[M.View.side]
    a.nvim_command("wincmd " .. move_to)
    a.nvim_command("vertical resize " .. get_defined_width())
    local winnr = a.nvim_get_current_win()
    local tabpage = a.nvim_get_current_tabpage()
    M.View.tabpages[tabpage] = vim.tbl_extend("force", M.View.tabpages[tabpage] or {}, { winnr = winnr })
    vim.cmd("buffer " .. M.View.bufnr)
    for k, v in pairs(M.View.winopts) do
        set_local(k, v)
    end
    vim.cmd(":wincmd =")
    if not options.focus then
        vim.cmd("wincmd p")
    end
end

function M.close()
    if not M.is_win_open() then
        return
    end
    if #a.nvim_list_wins() == 1 then
        local modified_buffers = utils.get_existing_buffers({ modified = true })

        if #modified_buffers == 0 then
            a.nvim_command(":silent q!")
        else
            utils.echo_warning("cannot exit with modified buffers!")
            a.nvim_command(":sb " .. modified_buffers[1])
        end
    end
    a.nvim_win_hide(M.get_winnr())
end

--- Returns the window number for sidebar-nvim within the tabpage specified
---@param tabpage number: (optional) the number of the chosen tabpage. Defaults to current tabpage.
---@return number
function M.get_winnr(tabpage)
    tabpage = tabpage or a.nvim_get_current_tabpage()
    local tabinfo = M.View.tabpages[tabpage]
    if tabinfo ~= nil then
        return tabinfo.winnr
    end
end

--- Returns the window width for sidebar-nvim within the tabpage specified
---@param tabpage number: (optional) the number of the chosen tabpage. Defaults to current tabpage.
---@return number
function M.get_width(tabpage)
    local winnr = M.get_winnr(tabpage)
    return vim.fn.winwidth(winnr)
end

return M


================================================
FILE: lua/sidebar-nvim.lua
================================================
local lib = require("sidebar-nvim.lib")
local colors = require("sidebar-nvim.colors")
local renderer = require("sidebar-nvim.renderer")
local view = require("sidebar-nvim.view")
local updater = require("sidebar-nvim.updater")
local config = require("sidebar-nvim.config")
local bindings = require("sidebar-nvim.bindings")
local profile = require("sidebar-nvim.profile")
local utils = require("sidebar-nvim.utils")

local M = { open_on_start = false, setup_called = false }

local deprecated_config_map = { docker = "containers" }
local function check_deprecated_field(key)
    if not vim.tbl_contains(vim.tbl_keys(deprecated_config_map), key) then
        return
    end

    local new_key = deprecated_config_map[key]
    utils.echo_warning("config '" .. key .. "' is deprecated. Please use '" .. new_key .. "' instead")
end

function M.setup(opts)
    opts = opts or {}

    -- this keys should not be merged by tbl_deep_merge, they should be overriden completely
    local full_override_keys = { "sections", "section_separator", "section_title_separator" }

    for key, value in pairs(opts) do
        check_deprecated_field(key)

        if key == "open" then
            M.open_on_start = value
        else
            if type(value) ~= "table" or vim.tbl_contains(full_override_keys, key) then
                config[key] = value
            else
                if type(config[key]) == "table" then
                    config[key] = vim.tbl_deep_extend("force", config[key], value)
                else
                    config[key] = value
                end
            end
        end
    end

    M.setup_called = true
    -- check if vim enter has already been called, if so, do initialize
    -- docs for `vim.v.vim_did_enter`: https://neovim.io/doc/user/autocmd.html#VimEnter
    if vim.v.vim_did_enter == 1 then
        M._internal_setup()
    end
end

function M._internal_setup()
    colors.setup()
    bindings.setup()
    view.setup()

    updater.setup()
    lib.setup()

    if M.open_on_start then
        M._internal_open()
    end
end

function M._vim_enter()
    if M.setup_called then
        M._internal_setup()
    end
end

-- toggle the sidebar
-- @param (table) opts (optional)
-- |- boolean opts.focus whether it should focus once open or not
function M.toggle(opts)
    lib.toggle(opts)
end

function M.close()
    lib.close()
end

function M._internal_open(opts)
    if not lib.is_open() then
        lib.open(opts)
    end
end

function M.open()
    M._internal_open()
end

-- Force immediate update
function M.update()
    lib.update()
end

-- Resize the sidebar to the requested size
-- @param size number
function M.resize(size)
    lib.resize(size)
    lib.update()
end

--- Returns the window width for sidebar-nvim within the tabpage specified
---@param tabpage number: (optional) the number of the chosen tabpage. Defaults to current tabpage.
---@return number
function M.get_width(tabpage)
    return lib.get_width(tabpage)
end

-- Focus or open the sidebar
-- @param opts table
-- @param opts.section_index number
-- @param opts.cursor_at_content boolean
function M.focus(opts)
    lib.focus(opts)
end

-- @param opts table
-- @param |- opts.any_tabpage boolean if true check if is open in any tabpage, if false check in current tab
function M.is_open(opts)
    return lib.is_open(opts)
end

function M.reset_highlight()
    if M.setup_called then
        colors.setup()
        renderer.render_hl(view.View.bufnr, {})
    end
end

function M._on_cursor_move(direction)
    lib.on_cursor_move(direction)
end

function M.print_profile_summary()
    if not config.enable_profile then
        utils.echo_warning("Profile not enabled")
        return
    end

    profile.print_summary()
end

return M


================================================
FILE: plugin/sidebar-nvim.vim
================================================
if !has('nvim-0.5') || exists('g:loaded_sidebar_nvim') | finish | endif

let s:save_cpo = &cpo
set cpo&vim

augroup SidebarNvim
au!
au VimEnter * lua require'sidebar-nvim'._vim_enter()
au VimLeavePre * lua require'sidebar-nvim.lib'.on_vim_leave()
au TabEnter * lua require'sidebar-nvim.lib'.on_tab_change()
au WinClosed * lua require'sidebar-nvim.lib'.on_win_leave()
au BufWritePost * lua require'sidebar-nvim.lib'.update()
au VimResume * lua require'sidebar-nvim.lib'.update()
au FocusGained * lua require'sidebar-nvim.lib'.update()
augroup end

command! SidebarNvimOpen lua require'sidebar-nvim'.open()
command! SidebarNvimClose lua require'sidebar-nvim'.close()
command! SidebarNvimToggle lua require'sidebar-nvim'.toggle()
command! SidebarNvimUpdate lua require'sidebar-nvim'.update()
command! SidebarNvimFocus lua require'sidebar-nvim'.focus()
command! -nargs=1 SidebarNvimResize lua require'sidebar-nvim'.resize(<args>)

let &cpo = s:save_cpo
unlet s:save_cpo

let g:loaded_sidebar_nvim = 1


================================================
FILE: selene.toml
================================================
std="vim"


================================================
FILE: stylua.toml
================================================
column_width = 120
indent_type = "Spaces"


================================================
FILE: vim.toml
================================================
[selene]
base = "lua51"
name = "vim"

[vim]
any = true

[[describe.args]]
type = "string"
[[describe.args]]
type = "function"

[[it.args]]
type = "string"
[[it.args]]
type = "function"

[[before_each.args]]
type = "function"
[[after_each.args]]
type = "function"

[assert.is_not]
any = true

[[assert.equals.args]]
type = "any"
[[assert.equals.args]]
type = "any"
[[assert.equals.args]]
type = "any"
required = false

[[assert.same.args]]
type = "any"
[[assert.same.args]]
type = "any"

[[assert.truthy.args]]
type = "any"

[[assert.spy.args]]
type = "any"

[[assert.stub.args]]
type = "any"


Download .txt
gitextract_r5xv7lik/

├── .editorconfig
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       ├── docgen.yaml
│       └── lint.yaml
├── .gitignore
├── .luacheckrc
├── README.md
├── doc/
│   ├── general.md
│   └── sidebar.txt
├── lua/
│   ├── sidebar-nvim/
│   │   ├── bindings.lua
│   │   ├── builtin/
│   │   │   ├── buffers.lua
│   │   │   ├── containers.lua
│   │   │   ├── datetime.lua
│   │   │   ├── diagnostics.lua
│   │   │   ├── files.lua
│   │   │   ├── git-status.lua
│   │   │   ├── git.lua
│   │   │   ├── lsp-diagnostics.lua
│   │   │   ├── symbols.lua
│   │   │   └── todos.lua
│   │   ├── colors.lua
│   │   ├── components/
│   │   │   ├── basic.lua
│   │   │   └── loclist.lua
│   │   ├── config.lua
│   │   ├── debouncer.lua
│   │   ├── docker_utils.lua
│   │   ├── events.lua
│   │   ├── lib.lua
│   │   ├── profile.lua
│   │   ├── renderer.lua
│   │   ├── updater.lua
│   │   ├── utils.lua
│   │   └── view.lua
│   └── sidebar-nvim.lua
├── plugin/
│   └── sidebar-nvim.vim
├── selene.toml
├── stylua.toml
└── vim.toml
Condensed preview — 39 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (169K chars).
[
  {
    "path": ".editorconfig",
    "chars": 108,
    "preview": "root = true\n\n[*]\ninsert_final_newline = true\nend_of_line = lf\n\n[*.lua]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 652,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 587,
    "preview": "---\nname: Feature request\nabout: Suggest an idea\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is your feature req"
  },
  {
    "path": ".github/workflows/docgen.yaml",
    "chars": 1053,
    "preview": "name: Generate Documentation\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - dev\n      - feat/docs\n\njobs:\n  docs"
  },
  {
    "path": ".github/workflows/lint.yaml",
    "chars": 767,
    "preview": "name: Linting & Formatting\n\non:\n  push:\n    branches: [main, dev]\n  pull_request:\n\nconcurrency: \n  group: ci-${{ github."
  },
  {
    "path": ".gitignore",
    "chars": 20,
    "preview": ".DS_Store\n\ndoc/tags\n"
  },
  {
    "path": ".luacheckrc",
    "chars": 192,
    "preview": "-- vim: ft=lua tw=80\n\n-- Don't report unused self arguments of methods.\nself = false\n\nignore = {\n  \"631\",  -- max_line_l"
  },
  {
    "path": "README.md",
    "chars": 1738,
    "preview": "# sidebar.nvim\n\nA generic and modular lua sidebar inspired by [lualine](https://github.com/hoob3rt/lualine.nvim)\n\nDevelo"
  },
  {
    "path": "doc/general.md",
    "chars": 19136,
    "preview": "# Overview\n\nA generic and modular lua sidebar inspired by lualine\n\n# Installing\n\nYou can install sidebar using any packa"
  },
  {
    "path": "doc/sidebar.txt",
    "chars": 25925,
    "preview": "*sidebar.txt*             For NVIM v0.6.0            Last change: 2022 June 21\n\n========================================"
  },
  {
    "path": "lua/sidebar-nvim/bindings.lua",
    "chars": 3390,
    "preview": "local api = vim.api\nlocal utils = require(\"sidebar-nvim.utils\")\n\nlocal config = require(\"sidebar-nvim.config\")\n\nlocal M "
  },
  {
    "path": "lua/sidebar-nvim/builtin/buffers.lua",
    "chars": 5421,
    "preview": "local utils = require(\"sidebar-nvim.utils\")\nlocal view = require(\"sidebar-nvim.view\")\nlocal Loclist = require(\"sidebar-n"
  },
  {
    "path": "lua/sidebar-nvim/builtin/containers.lua",
    "chars": 4522,
    "preview": "local utils = require(\"sidebar-nvim.utils\")\nlocal docker_utils = require(\"sidebar-nvim.docker_utils\")\nlocal Loclist = re"
  },
  {
    "path": "lua/sidebar-nvim/builtin/datetime.lua",
    "chars": 2981,
    "preview": "local config = require(\"sidebar-nvim.config\")\nlocal utils = require(\"sidebar-nvim.utils\")\nlocal has_luatz, luatz = pcall"
  },
  {
    "path": "lua/sidebar-nvim/builtin/diagnostics.lua",
    "chars": 4303,
    "preview": "local Loclist = require(\"sidebar-nvim.components.loclist\")\nlocal config = require(\"sidebar-nvim.config\")\n\nlocal loclist "
  },
  {
    "path": "lua/sidebar-nvim/builtin/files.lua",
    "chars": 16539,
    "preview": "local utils = require(\"sidebar-nvim.utils\")\nlocal Loclist = require(\"sidebar-nvim.components.loclist\")\nlocal config = re"
  },
  {
    "path": "lua/sidebar-nvim/builtin/git-status.lua",
    "chars": 377,
    "preview": "local utils = require(\"sidebar-nvim.utils\")\n\nlocal git = require(\"sidebar-nvim.builtin.git\")\n\nlocal deprecated_git = vim"
  },
  {
    "path": "lua/sidebar-nvim/builtin/git.lua",
    "chars": 7448,
    "preview": "local utils = require(\"sidebar-nvim.utils\")\nlocal sidebar = require(\"sidebar-nvim\")\nlocal Loclist = require(\"sidebar-nvi"
  },
  {
    "path": "lua/sidebar-nvim/builtin/lsp-diagnostics.lua",
    "chars": 446,
    "preview": "local utils = require(\"sidebar-nvim.utils\")\n\nlocal diagnostics = require(\"sidebar-nvim.builtin.diagnostics\")\n\nlocal depr"
  },
  {
    "path": "lua/sidebar-nvim/builtin/symbols.lua",
    "chars": 5352,
    "preview": "local Loclist = require(\"sidebar-nvim.components.loclist\")\nlocal config = require(\"sidebar-nvim.config\")\nlocal view = re"
  },
  {
    "path": "lua/sidebar-nvim/builtin/todos.lua",
    "chars": 6654,
    "preview": "local utils = require(\"sidebar-nvim.utils\")\nlocal Loclist = require(\"sidebar-nvim.components.loclist\")\nlocal config = re"
  },
  {
    "path": "lua/sidebar-nvim/colors.lua",
    "chars": 1082,
    "preview": "local api = vim.api\n\nlocal M = {}\n\nlocal function get_hl_groups()\n    return {}\nend\n\nlocal function get_links()\n    retu"
  },
  {
    "path": "lua/sidebar-nvim/components/basic.lua",
    "chars": 337,
    "preview": "local Component = {}\n\n-- convert the current data structure into a list of lines + highlight groups\n-- @param (table) ct"
  },
  {
    "path": "lua/sidebar-nvim/components/loclist.lua",
    "chars": 10182,
    "preview": "local Component = require(\"sidebar-nvim.components.basic\")\n\nlocal Loclist = {}\n\nLoclist.DEFAULT_OPTIONS = {\n    groups ="
  },
  {
    "path": "lua/sidebar-nvim/config.lua",
    "chars": 923,
    "preview": "local M = {}\n\nM.disable_default_keybindings = 0\nM.bindings = nil\nM.side = \"left\"\nM.initial_width = 35\n\nM.hide_statusline"
  },
  {
    "path": "lua/sidebar-nvim/debouncer.lua",
    "chars": 874,
    "preview": "local luv = vim.loop\n\nlocal Debouncer = {}\n\nfunction Debouncer:new(fn, delay)\n    local o = { fn = fn, delay = delay, lo"
  },
  {
    "path": "lua/sidebar-nvim/docker_utils.lua",
    "chars": 1143,
    "preview": "local config = require(\"sidebar-nvim.config\")\nlocal luv = vim.loop\n\nlocal M = {}\n\nfunction M.get_docker_bin()\n    local "
  },
  {
    "path": "lua/sidebar-nvim/events.lua",
    "chars": 955,
    "preview": "local M = {}\n\nlocal global_handlers = {}\n\nlocal Event = { Ready = \"Ready\" }\n\nlocal function get_handlers(event_name)\n   "
  },
  {
    "path": "lua/sidebar-nvim/lib.lua",
    "chars": 8304,
    "preview": "local luv = vim.loop\nlocal api = vim.api\n\nlocal renderer = require(\"sidebar-nvim.renderer\")\nlocal view = require(\"sideba"
  },
  {
    "path": "lua/sidebar-nvim/profile.lua",
    "chars": 1318,
    "preview": "local config = require(\"sidebar-nvim.config\")\n\nlocal M = {}\n\nM.entries = {}\n\nfunction M.clear()\n    M.entries = {}\nend\n\n"
  },
  {
    "path": "lua/sidebar-nvim/renderer.lua",
    "chars": 6011,
    "preview": "local view = require(\"sidebar-nvim.view\")\nlocal config = require(\"sidebar-nvim.config\")\nlocal utils = require(\"sidebar-n"
  },
  {
    "path": "lua/sidebar-nvim/updater.lua",
    "chars": 2420,
    "preview": "local utils = require(\"sidebar-nvim.utils\")\nlocal view = require(\"sidebar-nvim.view\")\nlocal config = require(\"sidebar-nv"
  },
  {
    "path": "lua/sidebar-nvim/utils.lua",
    "chars": 4260,
    "preview": "local M = {}\nlocal api = vim.api\nlocal luv = vim.loop\n\nfunction M.echo_warning(msg)\n    api.nvim_command(\"echohl Warning"
  },
  {
    "path": "lua/sidebar-nvim/view.lua",
    "chars": 7599,
    "preview": "local bindings = require(\"sidebar-nvim.bindings\")\nlocal config = require(\"sidebar-nvim.config\")\nlocal utils = require(\"s"
  },
  {
    "path": "lua/sidebar-nvim.lua",
    "chars": 3746,
    "preview": "local lib = require(\"sidebar-nvim.lib\")\nlocal colors = require(\"sidebar-nvim.colors\")\nlocal renderer = require(\"sidebar-"
  },
  {
    "path": "plugin/sidebar-nvim.vim",
    "chars": 997,
    "preview": "if !has('nvim-0.5') || exists('g:loaded_sidebar_nvim') | finish | endif\n\nlet s:save_cpo = &cpo\nset cpo&vim\n\naugroup Side"
  },
  {
    "path": "selene.toml",
    "chars": 10,
    "preview": "std=\"vim\"\n"
  },
  {
    "path": "stylua.toml",
    "chars": 42,
    "preview": "column_width = 120\nindent_type = \"Spaces\"\n"
  },
  {
    "path": "vim.toml",
    "chars": 594,
    "preview": "[selene]\nbase = \"lua51\"\nname = \"vim\"\n\n[vim]\nany = true\n\n[[describe.args]]\ntype = \"string\"\n[[describe.args]]\ntype = \"func"
  }
]

About this extraction

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

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

Copied to clipboard!