diff --git a/.config/nvim/init.lua b/.config/nvim/init.lua new file mode 100644 index 0000000..edacb09 --- /dev/null +++ b/.config/nvim/init.lua @@ -0,0 +1,22 @@ +-- Basic settings +vim.o.number = true -- Enable line numbers +vim.o.tabstop = 2 -- Number of spaces a tab represents +vim.o.shiftwidth = 2 -- Number of spaces for each indentation +vim.o.expandtab = true -- Convert tabs to spaces +vim.o.smartindent = true -- Automatically indent new lines +vim.o.wrap = true -- Disable line wrapping +vim.o.cursorline = true -- Highlight the current line +vim.o.termguicolors = true -- Enable 24-bit RGB colors +vim.o.clipboard = "unnamedplus" + +-- Syntax highlighting and filetype plugins +vim.cmd('syntax enable') +vim.cmd('filetype plugin indent on') + +require('neo-tree').setup({ + filesystem = { + filtered_items = { + hide_dotfiles = false + } + } +}) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.codecov.yml b/.config/nvim/pack/tree/start/neo-tree.nvim/.codecov.yml new file mode 100644 index 0000000..7c4fa59 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.codecov.yml @@ -0,0 +1,10 @@ +coverage: + status: + project: + default: + informational: true + only_pulls: true + patch: + default: + informational: true + only_pulls: true diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.github/ISSUE_TEMPLATE/bug_report.yml b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..27e01ae --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,95 @@ +name: Bug Report +description: File a bug / issue. +title: "BUG: " +labels: [bug] +body: + - type: markdown + attributes: + value: | + **Before** reporting an issue, make sure to read [`:h neo-tree.txt`](https://github.com/nvim-neo-tree/neo-tree.nvim/blob/v3.x/doc/neo-tree.txt) and search [existing issues](https://github.com/nvim-neo-tree/neo-tree.nvim/issues). Usage questions such as ***"How do I...?"*** belong in [Discussions](https://github.com/nvim-neo-tree/neo-tree.nvim/discussions) and will be closed. + - type: checkboxes + attributes: + label: Did you check docs and existing issues? + description: Make sure you checked all of the below before submitting an issue + options: + - label: I have read all the docs. + required: true + - label: I have searched the existing issues. + required: true + - label: I have searched the existing discussions. + required: true + - type: input + attributes: + label: "Neovim Version (nvim -v)" + placeholder: "NVIM v0.10.3" + validations: + required: true + - type: input + attributes: + label: "Operating System / Version" + placeholder: "MacOS 11.5" + validations: + required: true + - type: textarea + attributes: + label: Describe the Bug + description: A clear and concise description of what the bug is. Please include any related errors you see in Neovim. + validations: + required: true + - type: textarea + attributes: + label: Screenshots, Traceback + description: Screenshot and traceback if exists. Not required. + validations: + required: false + - type: textarea + attributes: + label: Steps to Reproduce + description: Steps to reproduce the behavior. Describe with the exact commands and keypresses. + placeholder: | + 1. + 2. + 3. + validations: + required: true + - type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: true + - type: textarea + attributes: + label: Your Configuration + description: Minimal `init.lua` to reproduce this issue. Save as `repro.lua` and run with `nvim -u repro.lua` + value: | + -- template from https://lazy.folke.io/developers#reprolua, feel free to replace if you have your own minimal init.lua + vim.env.LAZY_STDPATH = ".repro" + load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))() + + require("lazy.minit").repro({ + spec = { + { + "nvim-neo-tree/neo-tree.nvim", + branch = "v3.x", -- or "main" + dependencies = { + "nvim-lua/plenary.nvim", + "nvim-tree/nvim-web-devicons", -- not strictly required, but recommended + "MunifTanjim/nui.nvim", + -- { "3rd/image.nvim", opts = {} }, -- Optional image support + }, + lazy = false, + ---@module "neo-tree" + ---@type neotree.Config? + opts = { + -- fill any relevant options here + }, + } + }, + }) + vim.g.mapleader = " " + vim.keymap.set("n", "e", "Neotree") + -- do anything else you need to do to reproduce the issue + render: Lua + validations: + required: true diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.github/ISSUE_TEMPLATE/feature_request.yml b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..47ea504 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,36 @@ +name: Feature Request +description: Suggest a new feature. +title: "FEATURE: " +labels: [enhancement] +body: + - type: checkboxes + attributes: + label: Did you check the docs? + description: Make sure you read all the docs before submitting a feature request. + options: + - label: I have read all the docs. + required: true + - type: textarea + validations: + required: true + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + - type: textarea + validations: + required: true + attributes: + label: Describe the solution you'd like. + description: A clear and concise description of what you want to happen. + - type: textarea + validations: + required: false + attributes: + label: Describe alternatives you've considered. + description: A clear and concise description of any alternative solutions or features you've considered. + - type: textarea + validations: + required: false + attributes: + label: Additional Context + description: Add any other context or screenshots about the feature request here. diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/.luarc-5.1.json b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/.luarc-5.1.json new file mode 100644 index 0000000..d3a9194 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/.luarc-5.1.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + "diagnostics": { + "libraryFiles": "Disable" + }, + "runtime": { + "version": "LuaJIT", + "path": [ + "lua/?.lua", + "lua/?/init.lua", + "library/?.lua", + "library/?/init.lua" + ] + }, + "workspace": { + "checkThirdParty": "Disable", + "library": [ + "$PWD/.dependencies/pack/vendor/start/plenary.nvim", + "$PWD/.dependencies/pack/vendor/start/nui.nvim", + "$PWD/.dependencies/pack/vendor/start/nvim-web-devicons", + "$PWD/.dependencies/pack/vendor/start/snacks.nvim", + "${3rd}/luassert", + "${3rd}/busted", + "${3rd}/luv", + "$VIMRUNTIME" + ], + "ignoreDir": [ + ".dependencies", + ".luarocks", + ".lua" + ] + } +} diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/.luarc-luajit-master.json b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/.luarc-luajit-master.json new file mode 100644 index 0000000..d3a9194 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/.luarc-luajit-master.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + "diagnostics": { + "libraryFiles": "Disable" + }, + "runtime": { + "version": "LuaJIT", + "path": [ + "lua/?.lua", + "lua/?/init.lua", + "library/?.lua", + "library/?/init.lua" + ] + }, + "workspace": { + "checkThirdParty": "Disable", + "library": [ + "$PWD/.dependencies/pack/vendor/start/plenary.nvim", + "$PWD/.dependencies/pack/vendor/start/nui.nvim", + "$PWD/.dependencies/pack/vendor/start/nvim-web-devicons", + "$PWD/.dependencies/pack/vendor/start/snacks.nvim", + "${3rd}/luassert", + "${3rd}/busted", + "${3rd}/luv", + "$VIMRUNTIME" + ], + "ignoreDir": [ + ".dependencies", + ".luarocks", + ".lua" + ] + } +} diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/ci.yml b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/ci.yml new file mode 100644 index 0000000..677166d --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/ci.yml @@ -0,0 +1,84 @@ +name: CI +on: + push: + branches: + - main + - v1.x + - v2.x + - v3.x + pull_request: + workflow_dispatch: + +jobs: + stylua-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check formatting + uses: JohnnyMorganz/stylua-action@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + version: latest + args: --color always --check lua/ + + plenary-tests: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + rev: nightly/nvim-linux-x86_64.tar.gz + - os: ubuntu-22.04 + rev: v0.8.3/nvim-linux64.tar.gz + - os: ubuntu-22.04 + rev: v0.9.5/nvim-linux64.tar.gz + - os: ubuntu-22.04 + rev: v0.10.4/nvim-linux-x86_64.tar.gz + steps: + - uses: actions/checkout@v4 + - run: date +%F > todays-date + - name: Restore cache for today's nightly. + uses: actions/cache@v4 + with: + path: build + key: ${{ runner.os }}-${{ matrix.rev }}-${{ hashFiles('todays-date') }} + - name: Prepare + run: | + test -d build || { + mkdir -p build + curl -sL "https://github.com/neovim/neovim/releases/download/${{ matrix.rev }}" | tar xzf - --strip-components=1 -C "${PWD}/build" + } + + # - name: Get Luver Cache Key + # id: luver-cache-key + # env: + # CI_RUNNER_OS: ${{ runner.os }} + # run: | + # echo "::set-output name=value::${CI_RUNNER_OS}-luver-v1-$(date -u +%Y-%m-%d)" + # shell: bash + # - name: Setup Luver Cache + # uses: actions/cache@v2 + # with: + # path: ~/.local/share/luver + # key: ${{ steps.luver-cache-key.outputs.value }} + + # - name: Setup Lua + # uses: MunifTanjim/luver-action@v1 + # with: + # default: 5.1.5 + # lua_versions: 5.1.5 + # luarocks_versions: 5.1.5:3.8.0 + # - name: Setup luacov + # run: | + # luarocks install luacov + + - name: Run tests + run: | + export PATH="${PWD}/build/bin:${PATH}" + make setup + make test + + # - name: Upload coverage to Codecov + # uses: codecov/codecov-action@v2 diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/luals-check.yml b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/luals-check.yml new file mode 100644 index 0000000..383e7e5 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/luals-check.yml @@ -0,0 +1,44 @@ +name: Lua Language Server Diagnostics +on: + pull_request: ~ + push: + branches: + - '*' + +jobs: + luals-check: + strategy: + matrix: + neovim: ["0.10"] + lua: ["5.1", "luajit-master"] + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: luarocks/gh-actions-lua@v10 + with: + luaVersion: ${{matrix.lua}} + + - name: Install lua-language-server + uses: jdx/mise-action@v2 + with: + mise_toml: | + [tools] + neovim = "${{ matrix.neovim }}" + cargo-binstall = "latest" + "cargo:emmylua_check" = "latest" + "cargo:emmylua_ls" = "latest" + lua-language-server = "3.13.9" + + - name: Run lua-language-server check + run: | + LUARC=".github/workflows/.luarc-${{ matrix.lua }}.json" + make luals-check CONFIGURATION="$LUARC" + + - name: Run emmylua_check + continue-on-error: true # Doesn't type-check well enough to be worth erroring on, but this runs so fast we might as well help test this out. + run: | + LUARC=".github/workflows/.luarc-${{ matrix.lua }}.json" + make emmylua-check CONFIGURATION="$LUARC" diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/luarocks.yml b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/luarocks.yml new file mode 100644 index 0000000..5cf48a8 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/luarocks.yml @@ -0,0 +1,29 @@ +--- +name: Push to Luarocks + +on: + push: + tags: + - '*' + workflow_dispatch: + pull_request: # Will test the luarocks installation on PR, without uploading + +jobs: + luarocks-upload: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Required to count the commits + - name: Get Version + run: echo "LUAROCKS_VERSION=$(git describe --abbrev=0 --tags)" >> $GITHUB_ENV + - name: LuaRocks Upload + uses: nvim-neorocks/luarocks-tag-release@v5 + env: + LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }} + with: + version: ${{ env.LUAROCKS_VERSION }} + dependencies: | + plenary.nvim + nvim-web-devicons + nui.nvim diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/protect_release_branches.yml b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/protect_release_branches.yml new file mode 100644 index 0000000..70c6b0d --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.github/workflows/protect_release_branches.yml @@ -0,0 +1,29 @@ +# This is a basic workflow to help you get started with Actions + +name: No PRs to Release Branches + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the v1.x branch + pull_request: + types: [opened, edited, ready_for_review] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + check_target: + # The type of runner that the job will run on + runs-on: ubuntu-latest + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Runs a single command using the runners shell + - name: Fail when targeting v2 + run: | + target=${{ github.base_ref }} + echo "Target is: $target" + if [[ $target != "main" ]]; then + echo "PRs must target main" + exit 1 + else + exit 0 + fi diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.gitignore b/.config/nvim/pack/tree/start/neo-tree.nvim/.gitignore new file mode 100644 index 0000000..ff22e58 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.gitignore @@ -0,0 +1,51 @@ +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Vim tag files +tags + +# Others +.testcache +.dependencies +luacov.*.out + +tests/repro +.repro diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.lazy.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/.lazy.lua new file mode 100644 index 0000000..4d6685b --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.lazy.lua @@ -0,0 +1,24 @@ +local root = vim.fs.find({ "neo-tree.nvim" }, { upward = true })[1] +local deps_dir = root .. "/.dependencies/pack/vendor/start" +return { + { + "folke/snacks.nvim", + dir = deps_dir .. "/snacks.nvim", + }, + { + "nvim-neo-tree/neo-tree.nvim", + dir = root, + }, + { + "MunifTanjim/nui.nvim", + dir = deps_dir .. "/nui.nvim", + }, + { + "nvim-tree/nvim-web-devicons", + dir = deps_dir .. "/nvim-web-devicons", + }, + { + "nvim-lua/plenary.nvim", + dir = deps_dir .. "/plenary.nvim", + }, +} diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.luacov b/.config/nvim/pack/tree/start/neo-tree.nvim/.luacov new file mode 100644 index 0000000..400ec01 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.luacov @@ -0,0 +1,3 @@ +include = { + "lua%/neo%-tree", +} diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.luarc.json b/.config/nvim/pack/tree/start/neo-tree.nvim/.luarc.json new file mode 100644 index 0000000..ebec32b --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.luarc.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + "diagnostics": { + "libraryFiles": "Disable" + }, + "runtime": { + "version": "LuaJIT", + "path": [ + "lua/?.lua", + "lua/?/init.lua", + "library/?.lua", + "library/?/init.lua" + ] + }, + "workspace": { + "checkThirdParty": "Disable", + "library": [ + "$PWD/.dependencies/pack/vendor/start/plenary.nvim", + "$PWD/.dependencies/pack/vendor/start/nui.nvim", + "$PWD/.dependencies/pack/vendor/start/nvim-web-devicons", + "$PWD/.dependencies/pack/vendor/start/snacks.nvim", + "${3rd}/luassert", + "${3rd}/busted", + "${3rd}/luv", + "$VIMRUNTIME" + ], + "ignoreDir": [ + ".dependencies", + ".luarocks", + ".lua", + ".repro" + ] + } +} diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.stylua.toml b/.config/nvim/pack/tree/start/neo-tree.nvim/.stylua.toml new file mode 100644 index 0000000..e6c188a --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.stylua.toml @@ -0,0 +1,6 @@ +column_width = 100 +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 2 +quote_style = "AutoPreferDouble" +syntax = "LuaJIT" diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/.styluaignore b/.config/nvim/pack/tree/start/neo-tree.nvim/.styluaignore new file mode 100644 index 0000000..42ee163 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/.styluaignore @@ -0,0 +1 @@ +**/defaults.lua diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/CONTRIBUTING.md b/.config/nvim/pack/tree/start/neo-tree.nvim/CONTRIBUTING.md new file mode 100644 index 0000000..5c043c7 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# Contributing to Neo-tree + +Contributions are welcome! To keep everything clean and tidy, please follow the +guidelines below. + +## Code Style + +This is open for debate, but here is the current style choices being observed: + +- snake_case for all variables and functions +- unless it is a class, then use PascalCase +- other OOP things, like method names should use camelCase +- BUT we don't currently have any OOP parts and I don't think we want any + +I prefer `local name = function()` over `local function name()`, just to be +consistent with the `M.name = function()` exports. + +### StyLua + +We use (StyLua)[https://github.com/JohnnyMorganz/StyLua] to enforce consistency +in code. You should install it on your local machine. PRs will be checked with +this tool. + +## Commit Messages + +We use **semantic**, aka **conventional** commit messages. The official guide +can be found here: https://www.conventionalcommits.org/en/v1.0.0/ + +You can also just take a look at the commit history to get the idea. The +optional scope for this project would usually be the source, i.e. +`feat(filesystem): add awesome feature that does xyz`. + +## Branching + +The default branch is set to `main` and all Pull Requests should target this +branch. After a short testing period, it will be merged to the current release +branch. + +This project requires a **linear history**. I don't trust merge commits. +This means you will have to rebase your branch on main before the pull request +can be merged. This can get a bit annoying in a busy repository, but I think it +is worth the effort. + +## Documentation + +All new features should be documented in the commit they were added in. The +current strategy is to maintain: + +- Config Options: added to [defaults](lua/neo-tree/defaults.lua) and described + in comments. This is the bare minimum documentation for an option. +- The README contains "back of the box" high level overview of features. It is + meant for people trying to decide if they want to install this plugin or not. + It should include references to the help file for more information: + `:h neo-tree-setup` +- Whether something should be mentioned in the README or just in the help file + is a completely subjective judement call that is made on a case by case basis + based on how many people are likely to be interested in that information. +- The vim help file [doc/neo-tree.txt](doc/neo-tree.txt) is the definitive + reference and should contain all information needed to configure and use the + plugin. +- OUR DOCUMENTATION IS NOT GOOD ENOUGH! Consider the current level of documentation + the bare minumum and not the ideal. More documentation would be greatly appreciated. diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/Dockerfile b/.config/nvim/pack/tree/start/neo-tree.nvim/Dockerfile new file mode 100644 index 0000000..b7f91f9 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/Dockerfile @@ -0,0 +1,36 @@ +# --- Builder Stage --- +FROM alpine:latest AS builder + +RUN apk update && apk add --no-cache \ + build-base \ + ninja-build \ + cmake \ + coreutils \ + curl \ + gettext-tiny-dev \ + git + +# Install neovim +RUN git clone --depth=1 https://github.com/neovim/neovim --branch release-0.10 +RUN cd neovim && make CMAKE_BUILD_TYPE=RelWithDebInfo && make install + +# --- Final Stage --- +FROM alpine:latest + +RUN apk update && apk add --no-cache \ + libstdc++ # Often needed for C++ applications + +COPY --from=builder /usr/local/bin/nvim /usr/local/bin/nvim +COPY --from=builder /usr/local/share /usr/local/share + +ARG PLUG_DIR="/root/.local/share/nvim/site/pack/packer/start" +RUN mkdir -p $PLUG_DIR + +RUN apk add --no-cache git # Git is needed to clone plugins in the final image + +RUN git clone --depth=1 https://github.com/nvim-lua/plenary.nvim $PLUG_DIR/plenary.nvim +RUN git clone --depth=1 https://github.com/MunifTanjim/nui.nvim $PLUG_DIR/nui.nvim +RUN git clone --depth=1 https://github.com/nvim-tree/nvim-web-devicons.git $PLUG_DIR/nvim-web-devicons +COPY . $PLUG_DIR/neo-tree.nvim + +WORKDIR $PLUG_DIR/neo-tree.nvim diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/LICENSE b/.config/nvim/pack/tree/start/neo-tree.nvim/LICENSE new file mode 100644 index 0000000..74eaaf8 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2021 cseickel (https://github.com/cseickel) and nvim-neo-tree +maintainers. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/Makefile b/.config/nvim/pack/tree/start/neo-tree.nvim/Makefile new file mode 100644 index 0000000..61ec0d6 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/Makefile @@ -0,0 +1,45 @@ +.PHONY: test +test: + nvim --headless --noplugin -u tests/mininit.lua -c "lua require('plenary.test_harness').test_directory('tests/neo-tree/', {minimal_init='tests/mininit.lua',sequential=true})" + +.PHONY: test-docker +test-docker: + docker build -t neo-tree . + docker run --rm neo-tree make test + +.PHONY: format +format: + stylua --glob '*.lua' --glob '!defaults.lua' . + +# Dependencies: + +DEPS := ${CURDIR}/.dependencies/pack/vendor/start + +$(DEPS): + mkdir -p "$(DEPS)" + +$(DEPS)/nui.nvim: $(DEPS) + @test -d "$(DEPS)/nui.nvim" || git clone https://github.com/MunifTanjim/nui.nvim "$(DEPS)/nui.nvim" + +$(DEPS)/nvim-web-devicons: $(DEPS) + @test -d "$(DEPS)/nvim-web-devicons" || git clone https://github.com/nvim-tree/nvim-web-devicons "$(DEPS)/nvim-web-devicons" + +$(DEPS)/plenary.nvim: $(DEPS) + @test -d "$(DEPS)/plenary.nvim" || git clone https://github.com/nvim-lua/plenary.nvim "$(DEPS)/plenary.nvim" + +$(DEPS)/snacks.nvim: $(DEPS) + @test -d "$(DEPS)/snacks.nvim" || git clone https://github.com/folke/snacks.nvim "$(DEPS)/snacks.nvim" + +setup: $(DEPS)/nui.nvim $(DEPS)/nvim-web-devicons $(DEPS)/plenary.nvim $(DEPS)/snacks.nvim + @echo "[setup] environment ready" + +.PHONY: clean +clean: + rm -rf "$(DEPS)" + +CONFIGURATION = ${CURDIR}/.luarc.json +luals-check: setup + VIMRUNTIME="`nvim --clean --headless --cmd 'lua io.write(vim.env.VIMRUNTIME)' --cmd 'quit'`" lua-language-server --configpath=$(CONFIGURATION) --check=. + +emmylua-check: setup + VIMRUNTIME="`nvim --clean --headless --cmd 'lua io.write(vim.env.VIMRUNTIME)' --cmd 'quit'`" emmylua_check -c $(CONFIGURATION) -i ".dependencies/**" -- . diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/README.md b/.config/nvim/pack/tree/start/neo-tree.nvim/README.md new file mode 100644 index 0000000..ff3d590 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/README.md @@ -0,0 +1,1111 @@ +# Neo-tree.nvim + +Neo-tree is a Neovim plugin to browse the file system and other tree like +structures in whatever style suits you, including sidebars, floating windows, +netrw split style, or all of them at once! + +This screenshot shows Neo-tree opened in the traditional sidebar layout: + +![Neo-tree file system +sidebar](https://github.com/nvim-neo-tree/resources/blob/main/images/Neo-tree-with-right-aligned-symbols.png) + +
+ + Neo-tree filesystem screenshot, Netrw Style + + +The below screenshot shows Neo-tree opened "netrw style" (`:Neotree +position=current`). When opened in this way, there is more room so the extra +detail columns can be shown. This screenshot also shows how the contents can be +sorted on any column. In this example, we are sorted on "Size" descending: + +![Neo-tree file system +details](https://github.com/nvim-neo-tree/resources/blob/main/images/Neo-tree-with-file-details-and-sort.png) + +
+ +### Breaking Changes BAD :bomb: :imp: + +The biggest and most important feature of Neo-tree is that we will never +knowingly push a breaking change and interrupt your day. Bugs happen, but +breaking changes can always be avoided. When breaking changes are needed, there +will be a new branch that you can opt into, when it is a good time for you. + +See [What is a Breaking Change?](#what-is-a-breaking-change) for details. + +See [Changelog +3.0](https://github.com/nvim-neo-tree/neo-tree.nvim/wiki/Changelog#30) for +breaking changes and deprecations in 3.0. + +### User Experience GOOD :slightly_smiling_face: :thumbsup: + +Aside from being polite about breaking changes, Neo-tree is also focused on the +little details of user experience. Everything should work exactly as you would +expect a sidebar to work without all of the glitchy behavior that is normally +accepted in (neo)vim sidebars. I can't stand glitchy behavior, and neither +should you! + +- Neo-tree won't let other buffers take over its window. +- Neo-tree won't leave its window scrolled to the last line when there is plenty +of room to display the whole tree. +- Neo-tree does not need to be manually refreshed (set +`use_libuv_file_watcher=true`) +- Neo-tree can intelligently follow the current file (set +`follow_current_file.enabled=true`) +- Neo-tree is thoughtful about maintaining or setting focus on the right node +- Neo-tree windows in different tabs are completely separate +- `respect_gitignore` actually works! + +> [!NOTE] +> Neo-tree is meant to be smooth, efficient, stable, and intuitive. If you find +> anything janky, slow, broken, or unintuitive, please open an issue so we can +> fix it. + +## Installation + +This plugin relies upon these two excellent library plugins: + +- [MunifTanjim/nui.nvim](https://github.com/MunifTanjim/nui.nvim) for all UI +components, including the tree! +- [nvim-lua/plenary.nvim](https://github.com/nvim-lua/plenary.nvim) for backend +utilities, such as scanning the filesystem. + +There are also some optional plugins that work with Neo-tree: + +- [nvim-tree/nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) for file icons. +- [antosha417/nvim-lsp-file-operations](https://github.com/antosha417/nvim-lsp-file-operations) for LSP-enhanced renames/etc. +- [folke/snacks.nvim](https://github.com/folke/snacks.nvim) for image previews, see Preview Mode section. + - [snacks.rename](https://github.com/folke/snacks.nvim/blob/main/docs/rename.md#neo-treenvim) can also work with + Neo-tree +- [3rd/image.nvim](https://github.com/3rd/image.nvim) for image previews. + - If both snacks.nvim and image.nvim are installed. Neo-tree currently will + try to preview with snacks.nvim first, then try image.nvim. +- [s1n7ax/nvim-window-picker](https://github.com/s1n7ax/nvim-window-picker) for `_with_window_picker` keymaps. + + +### mini.deps example: + +```lua +local add = MiniDeps.add + +add({ + source = 'nvim-neo-tree/neo-tree.nvim', + checkout = 'v3.x', + depends = { + "nvim-lua/plenary.nvim", + "MunifTanjim/nui.nvim", + "nvim-tree/nvim-web-devicons", -- optional, but recommended + } +}) +``` + +### lazy.nvim example: + +```lua +return { + { + "nvim-neo-tree/neo-tree.nvim", + branch = "v3.x", + dependencies = { + "nvim-lua/plenary.nvim", + "MunifTanjim/nui.nvim", + "nvim-tree/nvim-web-devicons", -- optional, but recommended + }, + lazy = false, -- neo-tree will lazily load itself + } +} +``` + +
+ + lazy.nvim example with all optional plugins: + + +```lua +return { + { + "nvim-neo-tree/neo-tree.nvim", + branch = "v3.x", + dependencies = { + "nvim-lua/plenary.nvim", + "MunifTanjim/nui.nvim", + "nvim-tree/nvim-web-devicons", + }, + }, + { + "antosha417/nvim-lsp-file-operations", + dependencies = { + "nvim-lua/plenary.nvim", + "nvim-neo-tree/neo-tree.nvim", -- makes sure that this loads after Neo-tree. + }, + config = function() + require("lsp-file-operations").setup() + end, + }, + { + "s1n7ax/nvim-window-picker", + version = "2.*", + config = function() + require("window-picker").setup({ + filter_rules = { + include_current_win = false, + autoselect_one = true, + -- filter using buffer options + bo = { + -- if the file type is one of following, the window will be ignored + filetype = { "neo-tree", "neo-tree-popup", "notify" }, + -- if the buffer type is one of following, the window will be ignored + buftype = { "terminal", "quickfix" }, + }, + }, + }) + end, + }, +} +``` + +
+ +
+ + Packer.nvim example: + + +```lua +use({ + "nvim-neo-tree/neo-tree.nvim", + branch = "v3.x", + requires = { + "nvim-lua/plenary.nvim", + "MunifTanjim/nui.nvim", + "nvim-tree/nvim-web-devicons", -- optional, but recommended + } +}) +``` + +
+ +
+ + vim.pack example (Neovim v0.12, still in development at time of writing): + + +```lua +vim.pack.add({ + { + src = 'https://github.com/nvim-neo-tree/neo-tree.nvim', + version = vim.version.range('3') + }, + -- dependencies + "nvim-lua/plenary.nvim", + "MunifTanjim/nui.nvim", + -- optional, but recommended + "nvim-tree/nvim-web-devicons", +}) +``` + +
+ +### Manual installation via `:h packages` + +See [doc/install.sh](doc/install.sh) and [doc/install.ps1](doc/install.ps1) for +POSIX/Windows respectively. + +## Post-install: Try it out! + +Try `:Neotree` to open Neo-tree as a sidebar, and press `?` while in Neo-tree to +open the keyboard help. + +> [!TIP] +> You can `:checkhealth neo-tree` to ensure you have all the required +> dependencies. It can also check that your config table looks correct. This is +> still in its early stages, so please file issues if you'd like to see more +> checks added or a check isn't working properly. + +## Configuration + +```lua +require('neo-tree').setup({ + -- options go here +}) +``` + +
+ + 💤 lazy.nvim/Neovim distro users: + + +The table passed into `setup()` has a type of `neotree.Config`. If you're on a +distro using lazy.nvim (e.g. LazyVim) or you just like the syntax, you might +want to consider using lazy.nvim's `opts` instead: + +```lua +return { + "nvim-neo-tree/neo-tree.nvim", + branch = "v3.x", + dependencies = { + "nvim-lua/plenary.nvim", + "MunifTanjim/nui.nvim", + "nvim-tree/nvim-web-devicons", -- optional, but recommended + }, + lazy = false, -- neo-tree will lazily load itself + ---@module 'neo-tree' + ---@type neotree.Config + opts = { + -- options go here + } +} +``` + +
+ +> [!NOTE] +> You do not need to call `setup()` for Neo-tree and its commands to work. `setup()` is only for configuration. + +
+ + Example configuration featuring many interesting settings: + + +```lua +vim.keymap.set("n", "e", "Neotree") +require("neo-tree").setup({ + close_if_last_window = false, -- Close Neo-tree if it is the last window left in the tab + popup_border_style = "NC", -- or "" to use 'winborder' on Neovim v0.11+ + enable_git_status = true, + enable_diagnostics = true, + open_files_do_not_replace_types = { "terminal", "trouble", "qf" }, -- when opening files, do not use windows containing these filetypes or buftypes + open_files_using_relative_paths = false, + sort_case_insensitive = false, -- used when sorting files and directories in the tree + sort_function = nil, -- use a custom function for sorting files and directories in the tree + -- sort_function = function (a,b) + -- if a.type == b.type then + -- return a.path > b.path + -- else + -- return a.type > b.type + -- end + -- end , -- this sorts files and directories descendantly + default_component_configs = { + container = { + enable_character_fade = true, + }, + indent = { + indent_size = 2, + padding = 1, -- extra padding on left hand side + -- indent guides + with_markers = true, + indent_marker = "│", + last_indent_marker = "└", + highlight = "NeoTreeIndentMarker", + -- expander config, needed for nesting files + with_expanders = nil, -- if nil and file nesting is enabled, will enable expanders + expander_collapsed = "", + expander_expanded = "", + expander_highlight = "NeoTreeExpander", + }, + icon = { + folder_closed = "", + folder_open = "", + folder_empty = "󰜌", + provider = function(icon, node, state) -- default icon provider utilizes nvim-web-devicons if available + if node.type == "file" or node.type == "terminal" then + local success, web_devicons = pcall(require, "nvim-web-devicons") + local name = node.type == "terminal" and "terminal" or node.name + if success then + local devicon, hl = web_devicons.get_icon(name) + icon.text = devicon or icon.text + icon.highlight = hl or icon.highlight + end + end + end, + -- The next two settings are only a fallback, if you use nvim-web-devicons and configure default icons there + -- then these will never be used. + default = "*", + highlight = "NeoTreeFileIcon", + }, + modified = { + symbol = "[+]", + highlight = "NeoTreeModified", + }, + name = { + trailing_slash = false, + use_git_status_colors = true, + highlight = "NeoTreeFileName", + }, + git_status = { + symbols = { + -- Change type + added = "", -- or "✚" + modified = "", -- or "" + deleted = "✖", -- this can only be used in the git_status source + renamed = "󰁕", -- this can only be used in the git_status source + -- Status type + untracked = "", + ignored = "", + unstaged = "󰄱", + staged = "", + conflict = "", + }, + }, + -- If you don't want to use these columns, you can set `enabled = false` for each of them individually + file_size = { + enabled = true, + width = 12, -- width of the column + required_width = 64, -- min width of window required to show this column + }, + type = { + enabled = true, + width = 10, -- width of the column + required_width = 122, -- min width of window required to show this column + }, + last_modified = { + enabled = true, + width = 20, -- width of the column + required_width = 88, -- min width of window required to show this column + }, + created = { + enabled = true, + width = 20, -- width of the column + required_width = 110, -- min width of window required to show this column + }, + symlink_target = { + enabled = false, + }, + }, + -- A list of functions, each representing a global custom command + -- that will be available in all sources (if not overridden in `opts[source_name].commands`) + -- see `:h neo-tree-custom-commands-global` + commands = {}, + window = { + position = "left", + width = 40, + mapping_options = { + noremap = true, + nowait = true, + }, + mappings = { + [""] = { + "toggle_node", + nowait = false, -- disable `nowait` if you have existing combos starting with this char that you want to use + }, + ["<2-LeftMouse>"] = "open", + [""] = "open", + [""] = "cancel", -- close preview or floating neo-tree window + ["P"] = { + "toggle_preview", + config = { + use_float = true, + use_snacks_image = true, + use_image_nvim = true, + }, + }, + -- Read `# Preview Mode` for more information + ["l"] = "focus_preview", + ["S"] = "open_split", + ["s"] = "open_vsplit", + -- ["S"] = "split_with_window_picker", + -- ["s"] = "vsplit_with_window_picker", + ["t"] = "open_tabnew", + -- [""] = "open_drop", + -- ["t"] = "open_tab_drop", + ["w"] = "open_with_window_picker", + --["P"] = "toggle_preview", -- enter preview mode, which shows the current node without focusing + ["C"] = "close_node", + -- ['C'] = 'close_all_subnodes', + ["z"] = "close_all_nodes", + --["Z"] = "expand_all_nodes", + --["Z"] = "expand_all_subnodes", + ["a"] = { + "add", + -- this command supports BASH style brace expansion ("x{a,b,c}" -> xa,xb,xc). see `:h neo-tree-file-actions` for details + -- some commands may take optional config options, see `:h neo-tree-mappings` for details + config = { + show_path = "none", -- "none", "relative", "absolute" + }, + }, + ["A"] = "add_directory", -- also accepts the optional config.show_path option like "add". this also supports BASH style brace expansion. + ["d"] = "delete", + ["r"] = "rename", + ["b"] = "rename_basename", + ["y"] = "copy_to_clipboard", + ["x"] = "cut_to_clipboard", + ["p"] = "paste_from_clipboard", + ["c"] = "copy", -- takes text input for destination, also accepts the optional config.show_path option like "add": + -- ["c"] = { + -- "copy", + -- config = { + -- show_path = "none" -- "none", "relative", "absolute" + -- } + --} + ["m"] = "move", -- takes text input for destination, also accepts the optional config.show_path option like "add". + ["q"] = "close_window", + ["R"] = "refresh", + ["?"] = "show_help", + ["<"] = "prev_source", + [">"] = "next_source", + ["i"] = "show_file_details", + -- ["i"] = { + -- "show_file_details", + -- -- format strings of the timestamps shown for date created and last modified (see `:h os.date()`) + -- -- both options accept a string or a function that takes in the date in seconds and returns a string to display + -- -- config = { + -- -- created_format = "%Y-%m-%d %I:%M %p", + -- -- modified_format = "relative", -- equivalent to the line below + -- -- modified_format = function(seconds) return require('neo-tree.utils').relative_date(seconds) end + -- -- } + -- }, + }, + }, + nesting_rules = {}, + filesystem = { + filtered_items = { + visible = false, -- when true, they will just be displayed differently than normal items + hide_dotfiles = true, + hide_gitignored = true, + hide_hidden = true, -- only works on Windows for hidden files/directories + hide_by_name = { + --"node_modules" + }, + hide_by_pattern = { -- uses glob style patterns + --"*.meta", + --"*/src/*/tsconfig.json", + }, + always_show = { -- remains visible even if other settings would normally hide it + --".gitignored", + }, + always_show_by_pattern = { -- uses glob style patterns + --".env*", + }, + never_show = { -- remains hidden even if visible is toggled to true, this overrides always_show + --".DS_Store", + --"thumbs.db" + }, + never_show_by_pattern = { -- uses glob style patterns + --".null-ls_*", + }, + }, + follow_current_file = { + enabled = false, -- This will find and focus the file in the active buffer every time + -- -- the current file is changed while the tree is open. + leave_dirs_open = false, -- `false` closes auto expanded dirs, such as with `:Neotree reveal` + }, + group_empty_dirs = false, -- when true, empty folders will be grouped together + hijack_netrw_behavior = "open_default", -- netrw disabled, opening a directory opens neo-tree + -- in whatever position is specified in window.position + -- "open_current", -- netrw disabled, opening a directory opens within the + -- window like netrw would, regardless of window.position + -- "disabled", -- netrw left alone, neo-tree does not handle opening dirs + use_libuv_file_watcher = false, -- This will use the OS level file watchers to detect changes + -- instead of relying on nvim autocmd events. + window = { + mappings = { + [""] = "navigate_up", + ["."] = "set_root", + ["H"] = "toggle_hidden", + ["/"] = "fuzzy_finder", + ["D"] = "fuzzy_finder_directory", + ["#"] = "fuzzy_sorter", -- fuzzy sorting using the fzy algorithm + -- ["D"] = "fuzzy_sorter_directory", + ["f"] = "filter_on_submit", + [""] = "clear_filter", + ["[g"] = "prev_git_modified", + ["]g"] = "next_git_modified", + ["o"] = { + "show_help", + nowait = false, + config = { title = "Order by", prefix_key = "o" }, + }, + ["oc"] = { "order_by_created", nowait = false }, + ["od"] = { "order_by_diagnostics", nowait = false }, + ["og"] = { "order_by_git_status", nowait = false }, + ["om"] = { "order_by_modified", nowait = false }, + ["on"] = { "order_by_name", nowait = false }, + ["os"] = { "order_by_size", nowait = false }, + ["ot"] = { "order_by_type", nowait = false }, + -- [''] = function(state) ... end, + }, + fuzzy_finder_mappings = { -- define keymaps for filter popup window in fuzzy_finder_mode + [""] = "move_cursor_down", + [""] = "move_cursor_down", + [""] = "move_cursor_up", + [""] = "move_cursor_up", + [""] = "close", + [""] = "close_keep_filter", + [""] = "close_clear_filter", + [""] = { "", raw = true }, + { + -- normal mode mappings + n = { + ["j"] = "move_cursor_down", + ["k"] = "move_cursor_up", + [""] = "close_keep_filter", + [""] = "close_clear_filter", + [""] = "close", + } + } + -- [""] = "noop", -- if you want to use normal mode + -- ["key"] = function(state, scroll_padding) ... end, + }, + }, + + commands = {}, -- Add a custom command or override a global one using the same function name + }, + buffers = { + follow_current_file = { + enabled = true, -- This will find and focus the file in the active buffer every time + -- -- the current file is changed while the tree is open. + leave_dirs_open = false, -- `false` closes auto expanded dirs, such as with `:Neotree reveal` + }, + group_empty_dirs = true, -- when true, empty folders will be grouped together + show_unloaded = true, + window = { + mappings = { + ["d"] = "buffer_delete", + ["bd"] = "buffer_delete", + [""] = "navigate_up", + ["."] = "set_root", + ["o"] = { + "show_help", + nowait = false, + config = { title = "Order by", prefix_key = "o" }, + }, + ["oc"] = { "order_by_created", nowait = false }, + ["od"] = { "order_by_diagnostics", nowait = false }, + ["om"] = { "order_by_modified", nowait = false }, + ["on"] = { "order_by_name", nowait = false }, + ["os"] = { "order_by_size", nowait = false }, + ["ot"] = { "order_by_type", nowait = false }, + }, + }, + }, + git_status = { + window = { + position = "float", + mappings = { + ["A"] = "git_add_all", + ["gu"] = "git_unstage_file", + ["gU"] = "git_undo_last_commit", + ["ga"] = "git_add_file", + ["gr"] = "git_revert_file", + ["gc"] = "git_commit", + ["gp"] = "git_push", + ["gg"] = "git_commit_and_push", + ["o"] = { + "show_help", + nowait = false, + config = { title = "Order by", prefix_key = "o" }, + }, + ["oc"] = { "order_by_created", nowait = false }, + ["od"] = { "order_by_diagnostics", nowait = false }, + ["om"] = { "order_by_modified", nowait = false }, + ["on"] = { "order_by_name", nowait = false }, + ["os"] = { "order_by_size", nowait = false }, + ["ot"] = { "order_by_type", nowait = false }, + }, + }, + }, +}) +``` + +
+ +See `:h neo-tree` for full documentation. You can also preview that online at +[doc/neo-tree.txt](doc/neo-tree.txt), although it's best viewed within Neovim. + +To see all of the default config options with commentary, you can view it online +at [lua/neo-tree/defaults.lua](lua/neo-tree/defaults.lua). You can also paste it +into a buffer after installing Neo-tree by running: + +``` +:lua require("neo-tree").paste_default_config() +``` + +
+ Diagnostics icons: + +If you want icons for diagnostic errors, you'll need to define them somewhere. +In Neovim v0.10+, you can configure them in vim.diagnostic.config(), like: + +```lua +vim.diagnostic.config({ + signs = { + text = { + [vim.diagnostic.severity.ERROR] = '', + [vim.diagnostic.severity.WARN] = '', + [vim.diagnostic.severity.INFO] = '', + [vim.diagnostic.severity.HINT] = '󰌵', + }, + } +}) +``` + +For older versions of Neovim: + +```lua +vim.fn.sign_define("DiagnosticSignError", { text = " ", texthl = "DiagnosticSignError" }) +vim.fn.sign_define("DiagnosticSignWarn", { text = " ", texthl = "DiagnosticSignWarn" }) +vim.fn.sign_define("DiagnosticSignInfo", { text = " ", texthl = "DiagnosticSignInfo" }) +vim.fn.sign_define("DiagnosticSignHint", { text = "󰌵", texthl = "DiagnosticSignHint" }) +``` + +
+ +## The `:Neotree` Command + +The single `:Neotree` command accepts a range of arguments that give you full +control over the details of what and where it will show. For example, the following +command will open a file browser on the right hand side, "revealing" the currently +active file: + +``` +:Neotree filesystem reveal right +``` + +Arguments can be specified as either a key=value pair or just as the value. The +key=value form is more verbose but may help with clarity. For example, the command +above can also be specified as: + +``` +:Neotree source=filesystem reveal=true position=right +``` + +All arguments are optional and can be specified in any order. If you issue the command +without any arguments, it will use default values for everything. For example: + +``` +:Neotree +``` + +will open the filesystem source on the left hand side and focus it, if you are using +the default config. + +### Tab Completion + +Neotree supports tab completion for all arguments. Once a given argument has a value, +it will stop suggesting those completions. It will also offer completions for paths. +The simplest way to disambiguate a path from another type of argument is to start +them with `/` or `./`. + +### Arguments + +Here is the full list of arguments you can use: + +#### `action` + +What to do. Can be one of: + +| Option | Description | +|--------|-------------| +| focus | Show and/or switch focus to the specified Neotree window. DEFAULT | +| show | Show the window, but keep focus on your current window. | +| close | Close the window(s) specified. Can be combined with "position" and/or "source" to specify which window(s) to close. | + +#### `source` + +What to show. Can be one of: + +| Option | Description | +|--------|-------------| +| filesystem | Show a file browser. DEFAULT | +| buffers | Show a list of currently open buffers. | +| git_status | Show the output of `git status` in a tree layout. | +| last | Equivalent to the last source used | + +#### `position` + +Where to show it, can be one of: + +| Option | Description | +|---------|-------------| +| left | Open as left hand sidebar. DEFAULT | +| right | Open as right hand sidebar. | +| top | Open as top window. | +| bottom | Open as bottom window. | +| float | Open as floating window. | +| current | Open within the current window, like netrw or vinegar would. | + +#### `toggle` + +This is a boolean flag. Adding this means that the window will be closed if it +is already open. + +#### `dir` + +The directory to set as the root/cwd of the specified window. If you include a +directory as one of the arguments, it will be assumed to be this option, you +don't need the full dir=/path. You may use any value that can be passed to the +'expand' function, such as `%:p:h:h` to specify two directories up from the +current file. For example: + +``` +:Neotree ./relative/path +:Neotree /home/user/relative/path +:Neotree dir=/home/user/relative/path +:Neotree position=current dir=relative/path +``` + +#### `git_base` + +The base that is used to calculate the git status for each dir/file. +By default it uses `HEAD`, so it shows all changes that are not yet committed. +You can for example work on a feature branch, and set it to `main`. It will +show all changes that happened on the feature branch and main since you +branched off. + +Any git ref, commit, tag, or sha will work. + +``` +:Neotree main +:Neotree v1.0 +:Neotree git_base=8fe34be +:Neotree git_base=HEAD +``` + +#### `reveal` + +This is a boolean flag. Adding this will make Neotree automatically find and +focus the current file when it opens. + +#### `reveal_file` + +A path to a file to reveal. This supersedes the "reveal" flag so there is no +need to specify both. Use this if you want to reveal something other than the +current file. If you include a path to a file as one of the arguments, it will +be assumed to be this option. Like "dir", you can pass any value that can be +passed to the 'expand' function. For example: + +``` +:Neotree reveal_file=/home/user/my/file.text +:Neotree position=current dir=%:p:h:h reveal_file=%:p +:Neotree current %:p:h:h %:p +``` + +One neat trick you can do with this is to open a Neotree window which is +focused on the file under the cursor using the `` keyword: + +``` +nnoremap gd :Neotree float reveal_file= reveal_force_cwd +``` + +#### `reveal_force_cwd` + +This is a boolean flag. Normally, if you use one of the reveal options and the +given file is not within the current working directory, you will be asked if you +want to change the current working directory. If you include this flag, it will +automatically change the directory without prompting. This option implies +"reveal", so you do not need to specify both. + +#### `selector` + +This is a boolean flag. When you specifically set this to false (`selector=false`) +neo-tree will disable the [source selector](#source-selector) for that neo-tree +instance. Otherwise, the source selector will depend on what you specified in +the configuration (`config.source_selector.{winbar,statusline}`). + +See `:h neo-tree-commands` for details and a full listing of available arguments. + +### File Nesting + +See `:h neo-tree-file-nesting` for more details about file nesting. + +### Netrw Hijack + +``` +:edit . +:[v]split . +``` + +If `"filesystem.window.position"` is set to `"current"`, or if you have specified +`filesystem.hijack_netrw_behavior = "open_current"`, then any command +that would open a directory will open neo-tree in the specified window. + +## Sources + +Neo-tree is built on the idea of supporting various sources. Sources are +basically interface implementations whose job it is to provide a list of +hierarchical items to be rendered, along with commands that are appropriate to +those items. + +### filesystem +The default source is `filesystem`, which displays your files and folders. This +is the default source in commands when none is specified. + +This source can be used to: +- Browse the filesystem +- Control the current working directory of nvim +- Add/Copy/Delete/Move/Rename files and directories +- Search the filesystem +- Monitor git status and lsp diagnostics for the current working directory + +### buffers +![Neo-tree buffers](https://github.com/nvim-neo-tree/resources/raw/main/images/Neo-tree-buffers.png) + +Another available source is `buffers`, which displays your open buffers. This is +the same list you would see from `:ls`. To show with the `buffers` list, use: + +``` +:Neotree buffers +``` + +### git_status +This view take the results of the `git status` command and display them in a +tree. It includes commands for adding, unstaging, reverting, and committing. + +The screenshot below shows the result of `:Neotree float git_status` while the +filesystem is open in a sidebar: + +![Neo-tree git_status](https://github.com/nvim-neo-tree/resources/raw/main/images/Neo-tree-git_status.png) + +You can specify a different git base here as well. But be aware that it is not +possible to unstage / revert a file that is already committed. + +``` +:Neotree float git_status git_base=main +``` + +### document_symbols + +![Neo-tree document_symbols](https://github.com/nvim-neo-tree/resources/raw/main/images/neo-tree-document-symbols.png) +The document_symbols source lists the symbols in the current document obtained +by the LSP request "textDocument/documentSymbols". It currently supports the +following features: +- [x] UI: + - [x] Display all symbols in the current file with symbol kinds + - [x] Symbols nesting + - [x] Configurable kinds' name and icon + - [x] Auto-refresh symbol list + - [x] Follow cursor +- [ ] Commands + - [x] Jump to symbols, open symbol in split,... (`open_split` and friends) + - [x] Rename symbols (`rename`) + - [x] Preview symbol (`preview` and friends) + - [ ] Hover docs + - [ ] Call hierarchy +- [x] LSP + - [x] LSP Support + - [x] LSP server selection (ignore, allow_only, use first, use all, etc.) +- [ ] CoC Support + +See #879 for the tracking issue of these features. + +This source is currently experimental, so in order to use it, you need to first +add `"document_symbols"` to `config.sources` and open it with the command +``` +:Neotree document_symbols +``` + +### External Sources + +There are more sources available as extensions that are managed outside of this +repository. See the +[wiki](https://github.com/nvim-neo-tree/neo-tree.nvim/wiki/External-Sources) for +more information. + +### Source Selector + +![Neo-tree source +selector](https://github.com/nvim-neo-tree/resources/raw/main/images/Neo-tree-source-selector.png) + +You can enable a clickable source selector in either the winbar (requires neovim +0.8+) or the statusline. To do so, set one of these options to `true`: + +```lua + require("neo-tree").setup({ + source_selector = { + winbar = false, + statusline = false + } + }) +``` + +There are many configuration options to change the style of these tabs. +See [lua/neo-tree/defaults.lua](lua/neo-tree/defaults.lua) for details. + +### Preview Mode + +`:h neo-tree-preview-mode` + +Preview mode will temporarily show whatever file the cursor is on without +switching focus from the Neo-tree window. By default, files will be previewed +in a new floating window. This can also be configured to automatically choose +an existing split by configuring the command like this: + +```lua +require("neo-tree").setup({ + window = { + mappings = { + ["P"] = { + "toggle_preview", + config = { + use_float = false, + -- use_image_nvim = true, + -- use_snacks_image = true, + -- title = 'Neo-tree Preview', + }, + }, + } + } +}) +``` + +Anything that causes Neo-tree to lose focus will end preview mode. When +`use_float = false`, the window that was taken over by preview mode will revert +back to whatever was shown in that window before preview mode began. + +You can choose a custom title for the floating window by setting the `title` +option in its config. + +If you want to work with the floating preview mode window in autocmds or other +custom code, the window will have the `neo-tree-preview` filetype. + +When preview mode is not using floats, the window will have the window local +variable `neo_tree_preview` set to `1` to indicate that it is being used as a +preview window. You can refer to this in statusline and winbar configs to mark a +window as being used as a preview. + +#### Image Support in Preview Mode + +If you have +[folke/snacks.nvim](https://github.com/folke/snacks.nvim/blob/main/docs/image.md) +or [3rd/image.nvim](https://github.com/3rd/image.nvim) installed, preview mode +supports image rendering by default using kitty graphics protocol or ueberzug +([Video](https://user-images.githubusercontent.com/41065736/277180763-b7152637-f310-43a5-b8c3-4bcba135629d.mp4)). + +However, if you do not want this feature, you can disable it by setting +`use_snacks_image = false` or `use_image_nvim = false` in the mappings config +mentioned above. + +## Configuration and Customization + +This is designed to be flexible. The way that is achieved is by making +everything a function, or a string that identifies a built-in function. All of +the built-in functions can be replaced with your own implementation, or you can +add new ones. + +Each node in the tree is created from the renderer specified for the given node +type, and each renderer is a list of component configs to be rendered in order. +Each component is a function, either built-in or specified in your config. Those +functions simply return the text and highlight group for the component. + +Additionally, there is an events system that you can hook into. If you want to +show some new data point related to your files, gather it in the `before_render` +event, create a component to display it, and reference that component in the +renderer for the `file` and/or `directory` type. + +Details on how to configure everything is in the help file at `:h +neo-tree-configuration` or online at +[neo-tree.txt](https://github.com/nvim-neo-tree/neo-tree.nvim/blob/main/doc/neo-tree.txt) + +Recipes for customizations can be found on the +[wiki](https://github.com/nvim-neo-tree/neo-tree.nvim/wiki/Recipes). Recipes +include things like adding a component to show the +[Harpoon](https://github.com/ThePrimeagen/harpoon) index for files, or +responding to the `"file_opened"` event to auto clear the search when you open a +file. + +## Why? + +There are many tree plugins for (Neo)vim, so why make another one? Well, I +wanted something that was: + +1. Easy to maintain and enhance. +2. Stable. +3. Easy to customize. + +### Easy to maintain and enhance + +This plugin is designed to grow and be flexible. This is accomplished by making +the code as decoupled and functional as possible. Hopefully new contributors +will find it easy to work with. + +One big difference between this plugin and the ones that came before it, which +is also what finally pushed me over the edge into making a new plugin, is that +we now have libraries to build upon that did not exist when other tree plugins +were created. Most notably, [nui.nvim](https://github.com/MunifTanjim/nui.nvim) +and [plenary.nvm](https://github.com/nvim-lua/plenary.nvim). Building upon +shared libraries will go a long way in making neo-tree easy to maintain. + +### Stable + +This project will have releases and release tags that follow a simplified +Semantic Versioning scheme. The quickstart instructions will always refer to the +latest stable major version. Following the **main** branch is for contributors +and those that always want bleeding edge. There will be branches for **v1.x**, +**v2.x**, etc which will receive updates after a short testing period in +**main**. You should be safe to follow those branches and be sure your tree +won't break in an update. There will also be tags for each release pushed to +those branches named **v1.1**, **v1.2**, etc. If stability is critical to you, +or a bug accidentally makes it into **v3.x**, you can use those tags instead. +It's possible we may backport bug fixes to those tags, but no guarantees on +that. + +There will never be a breaking change within a major version (1.x, 2.x, etc.) If +a breaking change is needed, there will be depracation warnings in the prior +major version, and the breaking change will happen in the next major version. + +### Easy to Customize + +Neo-tree follows in the spirit of plugins like +[lualine.nvim](https://github.com/nvim-lualine/lualine.nvim) and +[nvim-cokeline](https://github.com/noib3/nvim-cokeline). Everything will be +configurable and take either strings, tables, or functions. You can take sane +defaults or build your tree items from scratch. There should be the ability to +add any features you can think of through existing hooks in the setup function. + +## What is a Breaking Change? + +As of v1.30, a breaking change is defined as anything that _changes_ existing: + +- vim commands (`:Neotree`) +- configuration options that are passed into the `setup()` function +- `NeoTree*` highlight groups +- lua functions exported in the following modules that are not prefixed with +`_`: +* `neo-tree` +* `neo-tree.events` +* `neo-tree.sources.manager` +* `neo-tree.sources.*` (init.lua files) +* `neo-tree.sources.*.commands` +* `neo-tree.ui.renderer` +* `neo-tree.utils` + +If there are other functions you would like to use that are not yet considered +part of the public API, please open an issue so we can discuss it. + +## Contributions + +Contributions are encouraged. Please see [CONTRIBUTING](CONTRIBUTING.md) for +more details. + +## Acknowledgements + +### Maintainers + +First and foremost, this project is a community endeavor and would not survive +without the constant stream of features and bug fixes that comes from that +community. There have been many valued contributors, but a few have stepped up +to become maintainers that generously donate their time to guide the project, +help out others, and manage the issues. The current list of maintainers are: + +- @pynappo + +### Past maintainers: + +(in alphabetical order) + +- @cseickel +- @miversen33 +- @nhat-vo +- @pysan3 + +### Other Projects + +The design is heavily inspired by these excellent plugins: +- [lualine.nvim](https://github.com/nvim-lualine/lualine.nvim) +- [nvim-cokeline](https://github.com/noib3/nvim-cokeline) + +Everything I know about writing a tree control in lua, I learned from: +- [nvim-tree.lua](https://github.com/nvim-tree/nvim-tree.lua) + + + diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/doc/install.ps1 b/.config/nvim/pack/tree/start/neo-tree.nvim/doc/install.ps1 new file mode 100755 index 0000000..c4d0cf4 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/doc/install.ps1 @@ -0,0 +1,86 @@ +# An example install script for people using `:h packages` +$NVIM_APPNAME = if ($env:NVIM_APPNAME) { $env:NVIM_APPNAME } else { "nvim" } + +$NEOTREE_DATA_HOME = if ($env:XDG_DATA_HOME) { + Join-Path $env:XDG_DATA_HOME $NVIM_APPNAME +} else { + Join-Path $HOME ".local\share\$NVIM_APPNAME" +} + +########### +# Options # +########### + +# You can modify the /neo-tree*/ names here, depending on how you like to organize your packages. +$NEOTREE_DIR = Join-Path $NEOTREE_DATA_HOME "site\pack\neo-tree\start" +$NEOTREE_DEPS_DIR = Join-Path $NEOTREE_DATA_HOME "site\pack\neo-tree-deps\start" +$NEOTREE_OPTIONAL_DIR = Join-Path $NEOTREE_DATA_HOME "site\pack\neo-tree-optional\start" + +# Modify the optional plugins you want below: +$OPTIONAL_PLUGINS = @( + "https://github.com/nvim-tree/nvim-web-devicons.git" # for file icons + # "https://github.com/antosha417/nvim-lsp-file-operations.git" # for LSP-enhanced renames/etc. + # "https://github.com/folke/snacks.nvim.git" # for image previews + # "https://github.com/3rd/image.nvim.git" # for image previews + # "https://github.com/s1n7ax/nvim-window-picker.git" # for _with_window_picker keymaps +) + +########################### +# The rest of the script. # +########################### + +# Save the current directory +$ORIGINAL_DIR = Get-Location + +function Invoke-GitCloneSparse { + git clone --filter=blob:none @args + if ($LASTEXITCODE -ne 0) { + Write-Error "Git clone failed with exit code $LASTEXITCODE for arguments: $($args -join ' ')" + } +} + +New-Item -ItemType Directory -Path $NEOTREE_DIR, $NEOTREE_DEPS_DIR -Force | Out-Null + +Write-Host "Installing neo-tree..." +Set-Location $NEOTREE_DIR +Invoke-GitCloneSparse -b v3.x "https://github.com/nvim-neo-tree/neo-tree.nvim.git" + +Write-Host "Installing core dependencies..." +Set-Location $NEOTREE_DEPS_DIR +Invoke-GitCloneSparse "https://github.com/nvim-lua/plenary.nvim.git" +Invoke-GitCloneSparse "https://github.com/MunifTanjim/nui.nvim.git" + +if ($OPTIONAL_PLUGINS.Count -gt 0) { + Write-Host "Installing optional plugins..." + New-Item -ItemType Directory -Path $NEOTREE_OPTIONAL_DIR -Force | Out-Null + Set-Location $NEOTREE_OPTIONAL_DIR + + foreach ($repo in $OPTIONAL_PLUGINS) { + Invoke-GitCloneSparse $repo + } +} + +Write-Host "Regenerating help tags..." +$PLUGIN_BASE_DIRS = @( + $NEOTREE_DIR + $NEOTREE_DEPS_DIR + $NEOTREE_OPTIONAL_DIR +) + +foreach ($base_dir in $PLUGIN_BASE_DIRS) { + # Check if the base directory exists + if (Test-Path $base_dir -PathType Container) { + foreach ($doc_path in Get-ChildItem "$base_dir/*/doc" -Directory) { + Write-Host "Generating helptags for: $doc_path" + & nvim -u NONE --headless -c "helptags $doc_path" -c "q" + if ($LASTEXITCODE -ne 0) { + Write-Warning "Nvim helptags command failed for $doc_path. Exit code: $LASTEXITCODE" + } + } + } else { + Write-Host "Info: Base plugin directory not found, skipping for helptags: $base_dir" + } +} + +Write-Host "Installation complete!" +Set-Location $ORIGINAL_DIR diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/doc/install.sh b/.config/nvim/pack/tree/start/neo-tree.nvim/doc/install.sh new file mode 100755 index 0000000..ce56160 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/doc/install.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# An example install script for people using `:h packages` +export NEOTREE_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/${NVIM_APPNAME:-nvim}" + +########### +# Options # +########### + +# You can modify the /neo-tree*/ names here, depending on how you like to organize your packages. +export NEOTREE_DIR="${NEOTREE_DATA_HOME}/site/pack/neo-tree/start" +export NEOTREE_DEPS_DIR="${NEOTREE_DATA_HOME}/site/pack/neo-tree-deps/start" +export NEOTREE_OPTIONAL_DIR="${NEOTREE_DATA_HOME}/site/pack/neo-tree-optional/start" + +# Modify the optional plugins you want below: +declare -a OPTIONAL_PLUGINS=( + "https://github.com/nvim-tree/nvim-web-devicons.git" # for file icons + # "https://github.com/antosha417/nvim-lsp-file-operations.git" # for LSP-enhanced renames/etc. + # "https://github.com/folke/snacks.nvim.git" # for image previews + # "https://github.com/3rd/image.nvim.git" # for image previews + # "https://github.com/s1n7ax/nvim-window-picker.git" # for _with_window_picker keymaps +) + +########################### +# The rest of the script. # +########################### + +ORIGINAL_DIR="$(pwd)" + +clone_sparse() { + git clone --filter=blob:none "$@" +} + +mkdir -p "${NEOTREE_DIR}" "${NEOTREE_DEPS_DIR}" + +echo "Installing neo-tree..." +cd "${NEOTREE_DIR}" +clone_sparse -b v3.x https://github.com/nvim-neo-tree/neo-tree.nvim.git + +echo "Installing core dependencies..." +cd "${NEOTREE_DEPS_DIR}" +clone_sparse https://github.com/nvim-lua/plenary.nvim.git +clone_sparse https://github.com/MunifTanjim/nui.nvim.git + +if [ ${#OPTIONAL_PLUGINS[@]} -gt 0 ]; then + echo "Installing optional plugins..." + mkdir -p "${NEOTREE_OPTIONAL_DIR}" + cd "${NEOTREE_OPTIONAL_DIR}" + + for repo in "${OPTIONAL_PLUGINS[@]}"; do + clone_sparse "$repo" + done +fi + +echo "Regenerating help tags..." +declare -a PLUGIN_BASE_DIRS=( + "${NEOTREE_DIR}" + "${NEOTREE_DEPS_DIR}" + "${NEOTREE_OPTIONAL_DIR}" +) + +# Loop through each base directory and find all 'doc' subdirectories using glob +shopt -s nullglob # Enable nullglob for safe globbing (empty array if no matches) +for base_dir in "${PLUGIN_BASE_DIRS[@]}"; do + # Check if the base directory exists + if [ -d "$base_dir" ]; then + for doc_path in "${base_dir}"/*/doc; do + nvim -u NONE --headless -c "helptags ${doc_path}" -c "q" + done + fi +done +shopt -u nullglob # Disable nullglob + +echo "Installation complete!" +cd "${ORIGINAL_DIR}" diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/doc/neo-tree.txt b/.config/nvim/pack/tree/start/neo-tree.nvim/doc/neo-tree.txt new file mode 100644 index 0000000..53a7514 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/doc/neo-tree.txt @@ -0,0 +1,2016 @@ +*neo-tree.txt* Plugin to browse the file system and other tree like structures + +CONTENTS *neo-tree* +Introduction ................ |neo-tree-introduction| +Commands .................... |neo-tree-commands| +Mappings .................... |neo-tree-mappings| + Help ...................... |neo-tree-help| + Navigation ................ |neo-tree-navigation| + View Changes .............. |neo-tree-view-changes| + File Actions .............. |neo-tree-file-actions| + Filter .................... |neo-tree-filter| + Custom Mappings ........... |neo-tree-custom-mappings| + Custom Commands ........... |neo-tree-custom-commands| +Configuration ............... |neo-tree-configuration| + Setup ..................... |neo-tree-setup| + Source Selector ........... |neo-tree-source-selector| + Filtered Items ............ |neo-tree-filtered-items| + Preview Mode .............. |neo-tree-preview-mode| + Hijack Netrw Behavior ..... |neo-tree-netrw-hijack| + Component Configs ......... |neo-tree-component-configs| + Git Status ................ |neo-tree-git-status| + Diagnostics ............... |neo-tree-diagnostics| + Indent markers ............ |neo-tree-indent-markers| + Expanders ................. |neo-tree-expanders| + File nesting .............. |neo-tree-file-nesting| + Highlights ................ |neo-tree-highlights| + Events .................... |neo-tree-events| + Components and Renderers .. |neo-tree-renderers| + Buffer Variables .......... |neo-tree-buffer-variables| + Popups .................... |neo-tree-popups| +Other Sources ............... |neo-tree-sources| + Buffers ................... |neo-tree-buffers| + Git Status ................ |neo-tree-git-status-source| + Document Symbols .......... |neo-tree-document-symbols| + + +INTRODUCTION *neo-tree-introduction* + +Neo-tree is a plugin for nvim that can display tree structures in a sidebar, +floating window, or in a split. The current version includes a filesystem +browser, a buffer list, and a git status view. + +If you want to use this in splits like |netrw| instead of as a sidebar or +floating window, use the *Split commands, such as |NeoTreeRevealInSplit|. You +may use both styles at the same time if you want. + + +COMMANDS *:Neotree* *neo-tree-commands* + +Neo-tree does not define any default keybindings for nvim. The suggested +keybindings are: + + nnoremap / :Neotree toggle current reveal_force_cwd + nnoremap | :Neotree reveal + nnoremap gd :Neotree float reveal_file= reveal_force_cwd + nnoremap b :Neotree toggle show buffers right + nnoremap s :Neotree float git_status + +The single |:Neotree| command accepts a range of arguments that give you full +control over the details of what and where it will show. Arguments can be +specified as either a key=value pair or just as the value. The key=value form +is more verbose but may help with clarity. For example, the "buffers" command +above can also be specified as: +>vim + :Neotree action=show source=buffers position=right toggle=true +< +These arguments can be specified in any order. Here is the full list of +arguments you can use: + +action~ +What to do. Can be one of: + + focus : Show and/or switch focus to the specified Neotree window. DEFAULT + show : Show the window, but keep focus on your current window. + close : Close the window(s) specified. Can be combined with "position" + and/or "source" to specify which window(s) to close. + +source~ +What to show. Can be one of: + + filesystem : Show a file browser. DEFAULT + buffers : Show a list of currently open buffers. + git_status : Show the output of `git status` in a tree layout. + +position~ +Where to show it, can be one of: + + left : Open as left hand sidebar. DEFAULT + right : Open as right hand sidebar. + float : Open as floating window. + current : Open within the current window, like netrw or vinegar would. + +toggle~ +This is a boolean flag. Adding this means that the window will be closed if it +is already open. + +dir~ +The directory to set as the root/cwd of the specified window. If you include a +directory as one of the arguments, it will be assumed to be this option, you +don't need the full dir=/path. You may use any value that can be passed to the +'expand' function, such as `%:p:h:h` to specify two directories up from the +current file. + +git_base~ +The base that is used to calculate the git status for each dir/file. +By default it uses `HEAD`, so it shows all changes that are not yet committed. +You can for example work on a feature branch, and set it to `main`. It will +show all changes that happened on the feature branch and main since you +branched off. + +Any git ref, commit, tag, or sha will work. + +reveal~ +This is a boolean flag. Adding this will make Neotree automatically find and +focus the current file when it opens. + +reveal_file~ +A path to a file to reveal. This supersedes the "reveal" flag so there is no +need to specify both. Use this if you want to reveal something other than the +current file. If you include a path to a file as one of the arguments, it will +be assumed to be this option. Like "dir", you can pass any value that can be +passed to the 'expand' function. + +reveal_force_cwd~ +This is a boolean flag. Normally, if you use one of the reveal options and the +given file is not within the current working directory, you will be asked if you +want to change the current working directory. If you include this flag, it will +automatically change the directory without prompting. This option implies +"reveal", so you do not need to specify both. + +selector~ +This is a boolean flag. When you specifically set this to false (`selector=false`) +neo-tree will disable the |neo-tree-source-selector| for that neo-tree +instance. Otherwise, the source selector will depend on what you specified in +the configuration (`config.source_selector.{winbar,statusline}`). + +CALLING_FROM_LUA + +You can call Neotree commands from your Lua scripts as follows: + +>lua + require('neo-tree.command').execute({ ... }) +< + +For example, you could use the following keymap to make the `-` key reveal the +current file, or if in an unsaved file, the current working directory. + +>lua + vim.keymap.set('n', '-', function() + local reveal_file = vim.fn.expand('%:p') + if (reveal_file == '') then + reveal_file = vim.fn.getcwd() + else + local f = io.open(reveal_file, "r") + if (f) then + f.close(f) + else + reveal_file = vim.fn.getcwd() + end + end + require('neo-tree.command').execute({ + action = "focus", -- OPTIONAL, this is the default value + source = "filesystem", -- OPTIONAL, this is the default value + position = "left", -- OPTIONAL, this is the default value + reveal_file = reveal_file, -- path to file or folder to reveal + reveal_force_cwd = true, -- change cwd without asking if needed + }) + end, + { desc = "Open neo-tree at current file or working directory" } + ); +< + +=============================================================================== +MAPPINGS ~ +=============================================================================== + *neo-tree-mappings* + +HELP *neo-tree-help* + +? = show_help: Shows a popup window with all of the mappings for the current + Neotree window. Pressing one of those keys will close the help + screen and execute the chosen command in the original Neotree + window. NOTE that selecting a line in the help window and + pressing enter will not execute that command, it will just + execute whatever the enter key is mapped to. + + +NAVIGATION *neo-tree-navigation* + +Within the neo-tree window, for the filesystem source, the following mappings +are defined by default. All built-in commands are listed here but some are not +mapped by default. See |neo-tree-custom-commands| for details on how to use them +in a custom mapping. + +Note: The "selected" item is the line the cursor is currently on. + +< = prev_source: Switches to the previous source. + +> = next_source: Switches to the next source. + + = navigate_up: Moves the root directory up one level. + +. = set_root: Changes the root directory to the currently + selected folder. + + = toggle_node Expand or collapse a node with children, which + may be a directory or a nested file. + +<2-LeftMouse> = open: Expand or collapse a folder. If a file is selected, + open it in the window closest to the tree. + + = open: Same as above. + +C = close_node: Close node if it is open, else close it's parent. + +z = close_all_nodes: Close all nodes in the tree. + + close_all_subnodes: Same as "close_node", but also recursively collapse + all subnodes, similar to "close_all_nodes" + + expand_all_nodes: Expand all directory nodes in the tree recursively. + + expand_all_subnodes: Same as expand_all_nodes but defaults to + recursively expanding just the node under the + cursor. + +P = toggle_preview: Toggles "preview mode", see |neo-tree-preview-mode| + +l = focus_preview: Focus the active preview window + + = scroll_preview: Scrolls preview window down (without focusing it) + see |neo-tree-preview-mode| for params + + = scroll_preview: Scrolls preview window up (without focusing it) + see |neo-tree-preview-mode| for params + + = revert_preview: Ends "preview_mode" if it is enabled, and reverts + any preview windows to what was being shown before + preview mode began. + +S = open_split: Same as open, but opens in a new horizontal split. + +s = open_vsplit: Same as open, but opens in a vertical split. + + open_rightbelow_vs: Same as open_vsplit, but opens in a right window + of the vertical split. + + open_leftabove_vs: Same as open_vsplit, but opens in a left window + of the vertical split. + +t = open_tabnew: Same as open, but opens in a new tab. + + open_drop: Same as open, but opens with the |:drop| command. + + open_tab_drop: Same as open, but opens in a new tab with the + |:drop| command with the |:tab| modifier. + +w = open_with_window_picker: Uses the `window-picker` plugin to select a window + to open the selected node in. Requires that + https://github.com/s1n7ax/nvim-window-picker + be installed. + + split_with_window_picker: Same as `open_with_window_picker` but opens split + in selected node instead. + + vsplit_with_window_picker: Same as `open_with_window_picker` but opens + vertical split in selected node instead. + +[g = prev_git_modified: Jump to the previous file reported by `git status` + that is within the current working directory. + This will loop around if you are on the last one. + +]g = next_git_modified: Jump to the next file reported by `git status` + that is within the current working directory. + This will loop around if you are on the last one. + + +FILE ACTIONS *neo-tree-file-actions* +a = add: Create a new file OR directory. Add a `/` to the + end of the name to make a directory. This command + supports an optional `config.show_path` option + which controls what portion of the path is shown + in the prompt. The choices for this option are: + + `"none"`: which is the default. + `"relative"`: shows the portion which is relative + to the current root of the tree. + `"absolute"`: is the full path to the current + directory. + + The file path also supports BASH style brace + expansion. sequence style ("{00..05..2}") as well + as nested braces. Here are some examples how this + expansion works. + + "x{a..e..2}" : "xa", "xc", "xe" + "file.txt{,.bak}" : "file.txt", "file.txt.bak" + "./{a,b}/{00..02}.lua" : "./a/00.lua", "./a/01.lua", + "./a/02.lua", "./b/00.lua", + "./b/01.lua", "./b/02.lua" + +A = add_directory: Create a new directory, in this mode it does not + need to end with a `/`. The path also supports + BASH style brace expansion as explained in `add` + command. Also accepts `config.show_path` options + +d = delete: Delete the selected file or directory. + Supports visual selection.~ + +i = show_file_details Show file details in a popup window, such as size + and last modified date. + + This command supports two optional options to + change the format string for file timestamps, + `config.created_format` for the date created and + `config.modified_format` for the date last + modified (see `:h os.date()`). Instead of a + format string, these options also accept a + function that takes in the date in seconds and + returns a string to display. They also accept + "relative" as a valid format, which will format + the timestamp as a human-readable relative date. + + If the options are not set, they default to the + format string "%Y-%m-%d %I:%M %p". + +r = rename: Rename the selected file or directory. + +b = rename_basename: Rename the selected file or directory without the + extension. + +y = copy_to_clipboard: Mark file to be copied. + Supports visual selection.~ + +x = cut_to_clipboard: Mark file to be cut (moved). + Supports visual selection.~ + +p = paste_from_clipboard: Copy/move each marked file to the selected folder. + +c = copy: Copy the selected file or directory. + Also accepts the optional `config.show_path` option + like the add file action. + +m = move: Move the selected file or directory. + Also accepts the optional `config.show_path` option + like the add file action. + + +VIEW CHANGES *neo-tree-view-changes* +H = toggle_hidden: Toggle whether hidden (filtered items) are shown or not. + +R = refresh: Rescan the filesystem and redraw the tree. Changes made + within nvim should be detected automatically, but this is + useful for changes made elsewhere. + +o = order_by... Show help menu for order by choices. + +oc = ...created: Sort the tree by created date. + +od = ...diagnostics: Sort by diagnostic severity. + +og = ...git_status: Sort by git status. + +om = ...modified: Sort by last modified date. + +on = ...name: Sort by name (default sort). + +os = ...size: Sort by size. + +ot = ...type: Sort by type. + + +FILTER *neo-tree-filter* + +NOTE: All of the below commands are affected by the `find_by_full_path_words` +option: +>lua + require("neo-tree").setup({ + filesystem = { + find_by_full_path_words = false, + } + }) +< +`false` means it only searches the tail of a path and is the default. +`true` will change the filter into a full path search with space as an implicit +`".*"`, so `fi init` will match: `./sources/filesystem/init.lua` + + +/ = fuzzy_finder: Filter the tree recursively, searching for + files and folders that contain the specified term as + you type. This will use fd if it is installed, or + find, or which if you are on Windows. + + As of v1.28, this acts like a fuzzy finder, + meaning that pressing up/down while the filter + window is open will move the cursor up and down in + the tree, and pressing `` will open that + item and clear the filter. Any other method of + closing the filter window will also clear the + filter. + +D = fuzzy_finder_directory: Like fuzzy_finder above, but only shows directories. + Pressing on a directory will clear the + search and set the focus on that directory. + Requires `fd` or `find` to be installed~ + +# = fuzzy_sorter: Sort the tree recursively based on fzy algorithm, + showing top score files. Space separated keywords + are treated as `and` which will be useful to narrow + down as you type. The file list is taken from fd + and other programs mentioned in `fuzzy_finder`. + `fuzzy_sorter_directory` can be used to show list + of directories instead. + +f = filter_on_submit: Same as above, but does not search until you hit + enter. Useful if filter_as_you_type is too slow. + Also useful if you want to leave the tree + filtered. + + = clear_filter: Removes the filter. + +PREVIEW MODE *neo-tree-preview-mode* + +Preview mode will temporarily show whatever file the cursor is on without +switching focus from the Neo-tree window. By default, files will be previewed +in a new floating window. This can also be configured to automatically choose +an existing split by configuring the command like this: + +>lua + require("neo-tree").setup({ + window = { + mappings = { + ["P"] = { + "toggle_preview", + config = { + use_float = false, + use_snacks_image = true, + use_image_nvim = true + } + }, + ["l"] = "focus_preview", + [""] = { "scroll_preview", config = {direction = 10} }, + [""] = { "scroll_preview", config = {direction = -10} }, + } + } + }) +< +Anything that causes Neo-tree to lose focus will end preview mode. When +`use_float = false`, the window that was taken over by preview mode will revert +back to whatever was shown in that window before preview mode began. + +If you want to work with the floating preview mode window in autocmds or other +custom code, the window will have the `neo-tree-preview` filetype. + +When preview mode is not using floats, the window will have the window local +variable `neo_tree_preview` set to `1` to indicate that it is being used as a +preview window. You can refer to this in statusline and winbar configs to mark a +window as being used as a preview. + +If you have folke/snacks.nvim or 3rd/image.nvim installed, preview mode supports +image rendering by default using kitty graphics protocol or ueberzug. + +- https://github.com/folke/snacks.nvim/blob/main/docs/image.md +- https://github.com/3rd/image.nvim + +However, if you do not want this feature, you can disable it by setting +`use_snacks_image = false` or `use_image_nvim = false` in the mappings config +mentioned above. + +CUSTOM MAPPINGS *neo-tree-custom-mappings* + +If you want to change the mappings, you can do so in two places. Mappings +defined in `window.mappings` apply to all sources, and mappings defined at the +source level, such as `filesystem.window.mappings` will override and extend +those global mappings for that particular source. + +For example: +>lua + require("neo-tree").setup({ + window = { + mappings = { + ["A"] = "command_a", + ["i"] = { + function(state) + local node = state.tree:get_node() + print(node.path) + end, + desc = "print path", + }, + } + }, + filesystem = { + window = { + mappings = { + ["A"] = "command_b" + } + } + } + }) +< +The above config will map `A` to command_a for all sources except for +filesystem, which will use command_b instead. + +The desc is used to display in the help popup. + +If you don't want to use *any* default mappings, you can set +`use_default_mappings = false` in your config. + +If you want to remove one or more particular default mappings, you can map +the sequence to `none` or `noop`. For example, if you don’t wish to use +fuzzy finder (default mapping `/`), but instead rely on Neovim’s built-in +search functionality, you can do that like so this: + +>lua + require("neo-tree").setup({ + filesystem = { + window = { + mappings = { + -- disable fuzzy finder + ["/"] = "noop" + } + } + } + }) +< + +NOTE: Not all commands work for all sources. If it is defined in the source +section in the default config instead of at the root level, that means it is +specific to that source and will not work for others. + + +CUSTOM MAPPINGS WITH VISUAL MODE *neo-tree-custom-mappings-visual* + +If you want to create a mapping that supports visual mode, the way to do that +is to add a second command where the name is the same as the normal mode +command, but with `_visual` added to the end. Any mapping for this command will +then work in either normal or visual mode. + +The `_visual` version of the command will be called with a second argument +which is a list of the nodes that were selected when the command was called. + +For example, this is how the built-in `delete` command is defined: + +>lua + M.delete = function(state, callback) + local tree = state.tree + local node = tree:get_node() + fs_actions.delete_node(node.path, callback) + end + + M.delete_visual = function(state, selected_nodes, callback) + local paths_to_delete = {} + for _, node_to_delete in pairs(selected_nodes) do + table.insert(paths_to_delete, node_to_delete.path) + end + fs_actions.delete_nodes(paths_to_delete, callback) + end +< + +CUSTOM MAPPINGS WITH ARGUMENTS *neo-tree-custom-mappings-args* + +If you want to include options for your mappings, such as `nowait`, you can +set this for all mappings using the `mapping_options` key, or on individual +mappings by specifying them as a table that consists of the command and any +options you want to use. If both are specified, the mapping merges with and +overrides the global `mapping_options` + +The command can be either the string name of a built-in command, or a +function, and is specified either as the first element in the table or by +assigning it to the `command` key: +>lua + require("neo-tree").setup({ + filesystem = { + window = { + mapping_options = { + noremap = true, + nowait = false, + }, + mappings = { + ["?"] = { + function(state) + local node = state.tree:get_node() + print(node.name) + end, + desc = "print name", + nowait = true + }, + ["i"] = { + command = function(state) + local node = state.tree:get_node() + print(node.name) + end, + desc = "print name", + nowait = true, + }, + ["o"] = { + command = "open", + nowait = true + }, + ["O"] = { + "open", + nowait = true + }, + } + } + } + }) +< +See |:map-arguments| for possible values to include. "buffer" and "nnoremap" +are enabled by default. + +CUSTOM MAPPINGS WITH CONFIG *neo-tree-custom-mappings-config* + +Some mappings may accept an optional `config` table to control it's behavior. +When that is the case, the command is specified using the table syntax, and +the config options are in a table bound to the `config` key: +>lua + require("neo-tree").setup({ + filesystem = { + window = { + mappings = { + ["/"] = { + "fuzzy_finder", + config = { + title = "Filter" -- An empty string hides the title + } + }, + ["a"] = { + "add", + nowait = true + config = { + show_path = "none" -- "none", "relative", "absolute" + } + }, + } + } + } + }) +< +When the `config` key is used, it is added to the `state` argument that is +passed to the command function: +>lua + M.add = function(state, callback) + local show_path = state.config.show_path + ... +< + +CUSTOM COMMANDS *neo-tree-custom-commands* + +If you want to define your own command, you have two options: + 1. You can define (or override) a command in the `commands` section of the + config for each source, then reference that by name in a mapping. + 2. You can map directly to a function and skip defining a command. + +You probably want #2: +>lua + require("neo-tree").setup({ + filesystem = { + window = { + mappings = { + ["?"] = function(state) + local node = state.tree:get_node() + print(node.name) + end + } + } + } + }) +< +..or +>lua + local print_me = function(state) + local node = state.tree:get_node() + print(node.name) + end + + require("neo-tree").setup({ + filesystem = { + window = { + mappings = { + ["?"] = print_me + } + } + } + }) +< +...but if you want #1, here is how that works: + +>lua + require("neo-tree").setup({ + filesystem = { + commands = { + print_me = function(state) + local node = state.tree:get_node() + print(node.name) + end + }, + mappings = { + ["?"] = "print_me" + } + } + }) +< + +GLOBAL CUSTOM COMMANDS *neo-tree-custom-commands-global* + +You can also have global custom commands that will be added to all available +sources. If you need it you can then override it in a specific source. +>lua + require("neo-tree").setup({ + commands = { + hello = function() -- define a global "hello world" function + print("Hello world") + end + }, + + window = { + mappings = { + [""] = "hello" + -- define a global mapping to call 'hello' in every source + } + }, + + filesystem = { + commands = { + -- override implementation of the 'hello' action in filesystem source + hello = function() + print("Hello inside filesystem") + end + } + } + }) +< +Now when pressing `` in 'buffers' or 'git_status' it will print "Hello world", +but in 'filesystem' it will print "Hello inside filesystem". + + +================================================================================ +CONFIGURATION ~ +================================================================================ + *neo-tree-configuration* +Neo-tree is highly configurable and you should be able to make it do whatever +you want without having to change the internal code. Here are the ways you can +customize it: + +By setting config options in the |neo-tree-setup| function. This is for very +common items and is how you would configure most lua plugins. You can also +change the look by configuring the appropriate highlight groups, see +|neo-tree-highlights|. + +By creating custom mappings (see |neo-tree-mappings|). You can of course just +change what keys are mapped to which built-in functions, but you can also map +keys to a custom function and do whatever you want. See the wiki for some +examples: https://github.com/nvim-neo-tree/neo-tree.nvim/wiki/Recipes#commands + +By hooking into |neo-tree-events|. You can do things like always clear the +search after opening a file, or define a custom file opener to choose what +window will be used, or respond to file events like renames and moves. + +By configuring, rearranging, adding, or removing |neo-tree-renderers| for each +node type. The renderer is a list of components, such as "icon" and "name", +which determines how each node displayed. Use them as lego pieces to build what +you want to see. + +By adding or replacing |neo-tree-components|. Components are the functions +called by the renderers, and they return the text and highlight group to be +displayed. If you want to gather extra data just once per render to be used by a +custom component, you can do so in the "before_render" event (see +|neo-tree-events|), set that data on the `state` object, and reference it in the +component. See the wiki for some examples of custom components: +https://github.com/nvim-neo-tree/neo-tree.nvim/wiki/Recipes#components + + +SETUP *neo-tree-setup* + +To override the defaults or add new functionality, call the setup() function +with your overrides. For example, to add your own mappings in 'lua': + +>lua + require("neo-tree").setup({ + filesystem = { + window = { + mappings = { + [""] = "refresh", + ["o"] = "open", + } + } + } + }) +< + +NOTE: The mappings you define will be merged with the default mappings. If you +wish to remove a default mapping without overriding it with your own function, +assign it the string "none". This will cause it to be skipped and allow any +existing global mappings to work. + +NOTE: SOME OPTIONS ARE ONLY DOCUMENTED IN THE DEFAULT CONFIG!~ +Run `:lua require("neo-tree").paste_default_config()` to dump the fully +commented default config in your current file. Even if you don't want to use +that config as your starting point, you still may want to dump it to a blank +lua file just to read it as documentation. + +NOTE: Neo-tree has type annotations for its configuration. If you have a Lua +language server that supports EmmyLua/LuaCATS annotations, you can use +`neotree.Config`, for example: + +>lua + -- if using lazy.nvim: + { + "nvim-neo-tree/neo-tree.nvim", + dependencies = { + "nvim-lua/plenary.nvim", + "nvim-tree/nvim-web-devicons", + "MunifTanjim/nui.nvim", + -- { "3rd/image.nvim", opts = {} }, -- Optional image support + }, + lazy = false, -- neo-tree lazy-loads itself + ---@module "neo-tree" + ---@type neotree.Config? + -- opts = { + -- -- fill any relevant options here + -- }, + } +< + +SOURCE SELECTOR *neo-tree-source-selector* + +You can enable a clickable source selector in either the winbar +(requires neovim 0.8+) or the statusline. To do so, set one of these options +to `true`: + +>lua + requires("neo-tree").setup({ + source_selector = { + winbar = false, + statusline = false + } + }) +< + +The configuration options for source selector are all placed inside +`source_selector` table. Below are the options with their default values and +type notations. + +>lua + requires("neo-tree").setup({ + source_selector = { + winbar = false, -- toggle to show selector on winbar + statusline = false, -- toggle to show selector on statusline + show_scrolled_off_parent_node = false, -- boolean + sources = { -- table + { + source = "filesystem", -- string + display_name = " 󰉓 Files " -- string | nil + }, + { + source = "buffers", -- string + display_name = " 󰈚 Buffers " -- string | nil + }, + { + source = "git_status", -- string + display_name = " 󰊢 Git " -- string | nil + }, + }, + content_layout = "start", -- string + tabs_layout = "equal", -- string + truncation_character = "…", -- string + tabs_min_width = nil, -- int | nil + tabs_max_width = nil, -- int | nil + padding = 0, -- int | { left: int, right: int } + separator = { left = "▏", right= "▕" }, -- string | { left: string, right: string, override: string | nil } + separator_active = nil, -- string | { left: string, right: string, override: string | nil } | nil + show_separator_on_edge = false, -- boolean + highlight_tab = "NeoTreeTabInactive", -- string + highlight_tab_active = "NeoTreeTabActive", -- string + highlight_background = "NeoTreeTabInactive", -- string + highlight_separator = "NeoTreeTabSeparatorInactive", -- string + highlight_separator_active = "NeoTreeTabSeparatorActive", -- string + }, + }) +< + +Keywords: > + + ┃/ a \/ b \/ c \ ┃ <- edge of window + ^ ^^ ^^ ^ separators + left separator right separator +< + +Configuration Options: +When `show_scrolled_off_parent_node` is `true`, tabs are replaced with the parent +path of the top visible node when scrolled down. + +`sources` is a table to configure the contents of the bar. Previously known +as `tab_labels`. Sources should be a table of tables. Each table inside +`sources` must contain a `source` key, which will refer to the "name" of the +source to add to the bar. A second optional `display_name` key can be +provided to modify how you wish that source to appear in the bar. It is +safe to add sources that do not exist, they will simply be omitted from +the bar if they cannot be found. +NOTE: If `source_selector` is enabled (via `winbar=true` or `statusline=true`) +then the `default_source` will be updated to be the first entry of +`sources`. + +`content_layout` defines how the labels are placed inside a tab. This only takes +effect when the tab width is greater than the length of label i.e. +`tabs_layout = "equal", "focus"` or when `tabs_min_width` is large enough. +Following options are available. + 'start' : left aligned / 󰓩 bufname \/.. + 'end' : right aligned / 󰓩 bufname \/... + 'center' : centered with equal padding / 󰓩 bufname \/... + +`tabs_layout` defines how the tabs are aligned inside the window when there is +more than enough space. The following options are available. `active` will +expand the focused tab as much as possible. Bars denote the edge of window. + 'start' : left aligned ┃/ ~ \/ ~ \/ ~ \ ┃ + 'end' : right aligned ┃ / ~ \/ ~ \/ ~ \┃ + 'center' : centered with equal padding ┃ / ~ \/ ~ \/ ~ \ ┃ + 'equal' : expand all tabs equally to fit the window width ┃/ ~ \/ ~ \/ ~ \┃ + 'active' : expand the focused tab to fit the window width ┃/ focused tab \/ ~ \/ ~ \┃ + +`padding` defines the global padding of the source selector. It can be an +integer or a table with keys `left` and `right`. Setting `padding = 2` is +exactly the same as `{ left = 2, right = 2 }` + +`separator` and `separator_active` take string or table to define the separators +surrounding non-active and active tab respectively. When `separator_active` is +`nil`, it falls back to `separator`. They require three keys `left`, `right`, +and `override` which define how the separators of neighboring tabs are merged +together. The following four options are available for `override`. The examples +show the result of `{ left = "/", right = "\", override = ... }` + 'nil' : never merged / ~ \/ ~ \/ ~ \... + '"right"' : all merged to the right / ~ \ ~ \ ~ \... + '"left"' : all merged to the left / ~ / ~ / ~ /... + '"active"' : merged towards the active tab / ~ / focused tab \ ~ \... +When set to string such as "┃", it is equivalent to +`{ left = "┃", right = "┃", override = "active" }`. + +`show_separator_on_edge` takes a boolean value where `false` (default) hides the +separators on the far left / right. Especially useful when left and right +separator are the same. + 'true' : ┃/ ~ \/ ~ \/ ~ \┃ + 'false' : ┃ ~ \/ ~ \/ ~ ┃ + + +CURRENT WORKING DIRECTORY *neo-tree-cwd* + +By default, Neo-tree will maintain a two-way binding between the cwd of nvim and +the root of the tree. Changing the root in Neo-tree will change the working +directory of nvim and vice versa. + +In the case of a sidebar, this will be synced with the tab working directory +(|tcd|). If you open it in the "current" position, aka netrw style, it will sync +with the window local working directory (|lcd|). + +These defaults can be changed by setting the following options: + +>lua + require("neo-tree").setup({ + filesystem = { + bind_to_cwd = true, -- true creates a 2-way binding between vim's cwd and neo-tree's root + cwd_target = { + sidebar = "tab", -- sidebar is when position = left or right + current = "window" -- current is when position = current + }, + } + }) +< + +In addition to `"tab"` and `"window"`, you can also set the target to `"global"` +for either option, which is the same as using the |cd| command. Setting the target +to `"none"` will prevent neo-tree from setting vim's cwd for that position. + + +FILTERED ITEMS *neo-tree-filtered-items* + +The `filesystem` source has a `filtered_items` section in it's config that +allows you to specify what files and folders should be hidden. By default, any +item identified by these filters will not be visible, but that visibility can +be toggled on and off with a command. Each type of filter has a corresponding +highlight group which will be applied when they are visible, see +|neo-tree-highlights| for details. The following options are available: + +>lua + require("neo-tree").setup({ + filesystem = { + filtered_items = { + visible = false, -- when true, they will just be displayed differently than normal items + hide_dotfiles = true, + hide_gitignored = true, + hide_hidden = true, -- only works on Windows for hidden files/directories + hide_by_name = { + ".DS_Store", + "thumbs.db", + --"node_modules", + }, + hide_by_pattern = { + --"*.meta", + --"*/src/*/tsconfig.json", + }, + always_show = { -- remains visible even if other settings would normally hide it + --".gitignored", + }, + always_show_by_pattern = { -- uses glob style patterns + --".env*", + }, + never_show = { -- remains hidden even if visible is toggled to true, this overrides always_show + --".DS_Store", + --"thumbs.db", + }, + never_show_by_pattern = { -- uses glob style patterns + --".null-ls_*", + }, + }, + } + }) +< + +The `visible` option just defines the default value. This value is toggled by +the "toggle_hidden" command, which is mapped to H by default. + +The `hide_dotfiles` option just hides anything that starts with `. `(period). + +The `hide_gitignored` option will query git for the files and folders being +shown, and hide those that are marked as ignored. + +The `hide_hidden` option only will work on Windows using the Windows logic +that determines if a file or directory is hidden. + +The `hide_by_name` option is a list of file/folder names that should be +hidden. This is an exact match. + +The `hide_by_pattern` option uses glob syntax, which is converted to lua +patterns. No guarantees on how it handles advanced patterns. + +The `always_show` option is a list of file/folder names that will always be +visible, even if other settings would normally hide it. This section takes +precedence over all other options except for `never_show`. + +the `always_show_by_pattern` option is a list of file/folder patterns that +always will be visible. This section takes precedence over all other options +except `never_show`. + +The `never_show` option is the same as `hide_by_name`, except that those items +will remain hidden even if you toggle `visible` to true. This section takes +precedence over the others. + +The `never_show_by_pattern` option is the same as `hide_by_pattern`, except that +those items will remain hidden even if you toggle `visible` to true. This +section takes precedence over the others. + + +NETRW HIJACK BEHAVIOR *neo-tree-netrw-hijack* + +Neo-tree can and does hijack Netrw by default. This is configurable and can be +disabled if you use Netrw, or have other plugins that use Netrw functionality. +This can be controlled by setting the `filesystem.hijack_netrw_behavior` option +to one of: + +disabled Netrw left alone, neo-tree does not handle opening dirs. + +open_default (default) Netrw disabled, opening a directory opens neo-tree + in whatever position is specified in `window.position`. + +open_current Netrw disabled, opening a directory opens within the + window like netrw would, regardless of `window.position`. + +>lua + require("neo-tree").setup({ + filesystem = { + hijack_netrw_behavior = "open_default", + -- "open_current", + -- "disabled", + }) +< + + + + +COMPONENT CONFIGS *neo-tree-component-configs* + +The visual display of a node is made up of a series of components rendered in a +certain order and with certain configuration options. See |neo-tree-components| +for a deeper dive into customizing this aspect. If you wish to configure those +components in a universal way, the best place to do that is in the +`default_component_configs` section of the config. + +For example, to configure indent markers, you can apply your settings in each renderer +for each source, or just do it once in the default_component_configs section: + +>lua + require("neo-tree").setup({ + default_component_configs = { + indent = { + with_markers = true, + indent_marker = "│", + last_indent_marker = "└", + indent_size = 2, + }, + }, + }) +< +See |neo-tree-indent-markers| for more details. + +The default config has more examples of component configuration, use +|NeoTreePasteConfig| to view that default config. + + +GIT STATUS *neo-tree-git-status* + +By default, Neo-tree will attempt to get the git status for files in the +current directory. It will use this information to add markers to the right of +your files, and will set the highlight groups of files and directories. + +To disable this feature entirely, set `enable_git_status = false` in your +config when calling the setup function. To just disable colors on file or +directory names, you can set `use_git_status_colors = false` in the `name` +component of your renderer(s). + +Starting with 2.0, this will display symbols by default. The default symbols +will require a nerd font to be installed. To change these symbols, you can set +the following properties: +>lua + require("neo-tree").setup({ + default_component_configs = { + git_status = { + symbols = { + -- Change type + added = "✚", + deleted = "✖", + modified = "", + renamed = "󰁕", + -- Status type + untracked = "", + ignored = "", + unstaged = "󰄱", + staged = "", + conflict = "", + } + } + } + }) +< +To change the color of these symbols, you can edit the corresponding highlight +groups: + + NeoTreeGitAdded + NeoTreeGitConflict + NeoTreeGitDeleted + NeoTreeGitIgnored + NeoTreeGitModified + NeoTreeGitUntracked + +If you'd like to disable certain symbols, you can set them to an empty string. +For example, it is actually redundant to show the change type if you use the +default behavior of highlighting the file name according to the change type. +The following config will remove those change type symbols: +>lua + require("neo-tree").setup({ + default_component_configs = { + git_status = { + symbols = { + -- Change type + added = "", + deleted = "", + modified = "", + renamed = "", + -- Status type + untracked = "", + ignored = "", + unstaged = "󰄱", + staged = "", + conflict = "", + } + } + } + }) +< + +To revert to the previous behavior of passing the git status through as-is +with codes like `[M ]` for changed/unstaged, and `[ M]` for changed/staged, +you can set the `symbols` property to nil or false: +>lua + require("neo-tree").setup({ + default_component_configs = { + git_status = { + symbols = false + } + } + }) +< +See also: |neo-tree-git-status-source| + + +DIAGNOSTICS *neo-tree-diagnostics* + +By default, Neo-tree will display diagnostic symbols next to files. It will +display the highest severity level for files, and errors only for directories. +If you want to use symbols instead of "E", "W", "I", and H", you'll need to +define those somewhere in your nvim configuration. Here is an example: + +>lua + vim.diagnostic.config({ + signs = { + text = { + [vim.diagnostic.severity.ERROR] = '', + [vim.diagnostic.severity.WARN] = '', + [vim.diagnostic.severity.INFO] = '', + [vim.diagnostic.severity.HINT] = '󰌵', + }, + } + }) +< + +If you don't change the `signs` option from its default, e.g.: + +>lua + vim.diagnostic.config({ + signs = true + }) +< + +or are on Neovim v0.9 and below, then this will work instead: + +>lua + vim.fn.sign_define("DiagnosticSignError", + {text = " ", texthl = "DiagnosticSignError"}) + vim.fn.sign_define("DiagnosticSignWarn", + {text = " ", texthl = "DiagnosticSignWarn"}) + vim.fn.sign_define("DiagnosticSignInfo", + {text = " ", texthl = "DiagnosticSignInfo"}) + vim.fn.sign_define("DiagnosticSignHint", + {text = "󰌵", texthl = "DiagnosticSignHint"}) +< + +Alternatively, you can also specify the signs and/or highlights in the +neo-tree setup call like this: + +>lua + require("neo-tree").setup({ + default_component_configs = { + diagnostics = { + symbols = { + hint = "H", + info = "I", + warn = "!", + error = "X", + }, + highlights = { + hint = "DiagnosticSignHint", + info = "DiagnosticSignInfo", + warn = "DiagnosticSignWarn", + error = "DiagnosticSignError", + }, + }, + } + }) +< + +Anything not specified in the `default_component_configs` will fallback to the +`sign_define` method. + +To disable this feature entirely, set `enable_diagnostics = false` in your +config when calling the setup function. + + +INDENT MARKERS *neo-tree-indent-markers* + +By default, indent markers (aka indent guides) are enabled. In Neo-tree +indent is a component, so to edit indent markers, you can configure the +`indent` component: + +...at the global level: +>lua + require("neo-tree").setup({ + default_component_configs = { + indent = { + with_markers = true, + indent_marker = "│", + last_indent_marker = "└", + indent_size = 2, + }, + }, + }) +< + +...or in each renderer: +>lua + require("neo-tree").setup({ + filesystem = { + renderers = { + directory = { + { + "indent", + with_markers = true, + indent_marker = "│", + last_indent_marker = "└", + indent_size = 2, + }, + -- other components + }, + file = { + { + "indent", + with_markers = true, + indent_marker = "│", + last_indent_marker = "└", + indent_size = 2, + }, + -- other components + }, + } + } + }) +< + +You also can change the marker characters. To do this, you need change +`indent_marker` and `last_indent_marker` settings. + +To change highlight of indent markers, you need configure `NeoTreeIndentMarker` +highlight group. By default, it refers to `Normal` highlight. + + +EXPANDERS *neo-tree-expanders* +Is hightly recommended enable if file nesting is enabled (this is the default +behavior if `with_expanders` is nil). The config can be done inside the `indent` +component: +>lua + require("neo-tree").setup({ + default_component_configs = { + indent = { + with_expanders = true, + expander_collapsed = "", + expander_expanded = "", + expander_highlight = "NeoTreeExpander", + }, + }, + }) +< + +FILE NESTING *neo-tree-file-nesting* + +By default, file nesting is disabled since the `nesting_rules` table is empty. +To enable this feature, fill in the `nesting_rules` table with the following +structure to match files based on their extensions: +>lua + require("neo-tree").setup({ + nesting_rules = { + ["js"] = { "js.map" }, + } + }) +< +This will render: +> + FILENAME.js + FILENAME.js.map + +A more advanced usage are rules with patterns. Those can be mixed with file +extension based rules. In this case the rule key is not evaluated but a +pattern which is defined in a subkey. The pattern is a Lua pattern and can +have captures which can be used for file matching via globs. +If the matching should be case insensitive, add an option ignore_case to the +config: +>lua + require("neo-tree").setup({ + nesting_rules = { + ["package.json"] = { + pattern = "^package%.json$", -- <-- Lua pattern + files = { "package-lock.json", "yarn*" } -- <-- glob pattern + }, + ["go"] = { + pattern = "(.*)%.go$", -- <-- Lua pattern with capture + files = { "%1_test.go" }, -- <-- glob pattern with capture + }, + ["js-extended"] = { + pattern = "(.+)%.js$", + files = { "%1.js.map", "%1.min.js", "%1.d.ts" }, + }, + ["docker"] = { + pattern = "^dockerfile$", + ignore_case = true, + files = { ".dockerignore", "docker-compose.*", "dockerfile*" }, + } + } + }) +< +This will render: +> + FILENAME.js + FILENAME.js.map + FILENAME.min.js + FILENAME.d.ts + + FILENAME.go + FILENAME_test.go + + package.json + package-lock.json + yarn.lock + + Dockerfile + .dockerignore + docker-compose.yml +< + +The default mapping to expand/collapse nested files is . + +A `priority` field can be added to each rule to resolve conflicting rules. The +default priority is 100. In the event that two rules may match the same file, +the rule with higher priority will win. If the priorities are the same, the +rule with a lower key value will win. +>lua + require("neo-tree").setup({ + nesting_rules = { + ["bar"] = { + pattern = "(.+)%.bar$", + files = { "%1.baz" }, + priority = 100, + }, + ["foo"] = { + pattern = "(.+)%.foo$", + files = { "%1.baz" }, + priority = 200, + }, + -- Without the priorities, "bar" < "foo", so bar would apply first. + } + }) +< +This will render: +> + a.bar + a.foo + a.baz +< +HIGHLIGHTS *neo-tree-highlights* + +The following highlight groups are defined by this plugin. If you set any of +these yourself before the plugin loads, it will not be touched. If they do not +exist, they will be created. + +NeoTreeBufferNumber The buffer number shown in the buffers source. +NeoTreeCursorLine |hl-CursorLine| override in Neo-tree window. +NeoTreeDimText Greyed out text used in various places. +NeoTreeDirectoryIcon Directory icon. +NeoTreeDirectoryName Directory name. +NeoTreeDotfile Used for icons and names when dotfiles are filtered. +NeoTreeFileIcon File icon, when not overridden by devicons. +NeoTreeFileName File name, when not overwritten by another status. +NeoTreeFileNameOpened File name when the file is open. Not used yet. +NeoTreeFilterTerm The filter term, as displayed in the root node. +NeoTreeFloatBorder The border for pop-up windows. +NeoTreeFloatTitle Used for the title text of pop-ups when the border-style + is set to another style than "NC". This is derived + from NeoTreeFloatBorder. +NeoTreeTitleBar Used for the title bar of pop-ups, when the border-style + is set to "NC". This is derived from NeoTreeFloatBorder. +NeoTreeGitAdded File name when the git status is added. +NeoTreeGitConflict File name when the git status is conflict. +NeoTreeGitDeleted File name when the git status is deleted. +NeoTreeGitIgnored File name when the git status is ignored. +NeoTreeGitModified File name when the git status is modified. +NeoTreeGitUnstaged Used for git unstaged symbol. +NeoTreeGitUntracked File name when the git status is untracked. +NeoTreeGitStaged Used for git staged symbol. +NeoTreeHiddenByName Used for icons and names when `hide_by_name` is used. +NeoTreeIndentMarker The style of indentation markers (guides). By default, + the "Normal" highlight is used. +NeoTreeExpander Used for collapsed/expanded icons. +NeoTreeNormal |hl-Normal| override in Neo-tree window. +NeoTreeNormalNC |hl-NormalNC| override in Neo-tree window. +NeoTreeSignColumn |hl-SignColumn| override in Neo-tree window. +NeoTreeStats Used for "stat" columns like size, last modified, etc. +NeoTreeStatsHeader Used for the header (top line) of the above columns. +NeoTreeStatusLine |hl-StatusLine| override in Neo-tree window. +NeoTreeStatusLineNC |hl-StatusLineNC| override in Neo-tree window. +NeoTreeVertSplit |hl-VertSplit| override in Neo-tree window. +NeoTreeWinSeparator |hl-WinSeparator| override in Neo-tree window. +NeoTreeEndOfBuffer |hl-EndOfBuffer| override in Neo-tree window. +NeoTreeRootName The name of the root node. +NeoTreeSymbolicLinkTarget Symbolic link target. +NeoTreeTitleBar Used for the title bar of pop-ups, when the border-style + is set to "NC". This is derived from NeoTreeFloatBorder. +NeoTreeWindowsHidden Used for icons and names that are hidden on Windows. + + +EVENTS *neo-tree-events* + +Events are one way to customize the behavior of Neo-tree. You can add event +handlers to your config in the `event_handlers` section, which should be a list +of objects in the form: + +>lua + { + event = "event_name", + handler = function(arg) + -- do something, the value of arg varies by event. + end, + id = "optional unique id, only meaningful if you want to unsubscribe later" + } +< + +The following events are available: + +"before_render"~ +Fired after items have been collected from the source but before drawing the +nodes of the tree. This is the best place to gather additional data to be used +by components. The argument passed is the state of the source, which is also +passed to components and commands down the line. + +"after_render"~ +Fired after the tree has been rendered. The argument passed is the state of the +source, which is also passed to components and commands down the line. + +"file_added"~ +Fired after a file (or folder) has been created, either by using the "add" +command or by copy and paste. The arg is the full path to the new file. + +"file_deleted"~ +Fired after a file (or folder) has been deleted. The arg is the full path to the +deleted file. + +"file_moved"~ +Fired after a file (or folder) has been moved. The arg is a table containing +`source` and `destination` properties. + +"file_open_requested"~ +Fired just before a file is opened. The arg is a table containing the `state` +of the source being used, the `path` of the file to be opened, and `open_cmd`, +which is the open command that was requested. `open_cmd` will be either |edit|, +|split|, |vsplit|, |tabnew|. This function should return a table with a property called +`handled` which is true if the file open operation was handled, or false if it +was not. If `{ handled = true }` is not returned, the file will be opened using +the built-in logic. + +"file_opened"~ +Fired after a file has been opened. You might use this to auto-close the window +or clear the filter. The arg is the path of the file opened. + +"file_renamed"~ +Fired after a file (or folder) has been renamed. The arg is an table containing +`source` and `destination` properties. + +"neo_tree_buffer_enter"~ +Fired after entering a neo-tree buffer. It is also right after neo-tree applies +it's own settings, so it's the ideal place to apply any local settings you would +like to have. + +"neo_tree_buffer_leave"~ +Fired after a neo-tree buffer was exited. Technically it fires when entering a +buffer that is not neo-tree, when the last buffer enter event was neo-tree. + +"neo_tree_popup_buffer_enter"~ +Fired after entering a neo-tree popup buffer. This includes things such as file +rename prompts and filter inputs. It runs right after neo-tree applies it's own +settings, so it's the ideal place to apply any local settings you would like to +have. + +"neo_tree_popup_buffer_leave"~ +Fired after leaving a neo-tree popup buffer. + +"neo_tree_popup_input_ready"~ +Fired after NuiInput is ready. Use this event to default to normal mode etc. +This is fired inside a vim.schedule. + +>lua + { + event = "neo_tree_popup_input_ready", + handler = function() + -- enter input popup with normal mode by default. + vim.cmd("stopinsert") + end, + }, + { + event = "neo_tree_popup_input_ready", + ---@param args { bufnr: integer, winid: integer } + handler = function(args) + -- map to enter normal mode (by default closes prompt) + -- don't forget `opts.buffer` to specify the buffer of the popup. + vim.keymap.set("i", "", vim.cmd.stopinsert, { noremap = true, buffer = args.bufnr }) + end, + } +< + +"neo_tree_window_before_open"~ +Fired before opening a new Neo-tree window. Called with the following arg: + *neo-tree-window-event-args* +The event argument for all window events is a table with the following keys: + `winid` = the |winid| of the window being opened or closed. + `tabid` = id of the tab that the window is in. + `tabnr` = (deprecated) number of the tab that the window is in. + `source` = the name of the source that is in the window, such as "filesystem". + `position` = the position of the window, i.e. "left", "bottom", "right". + +"neo_tree_window_after_open"~ +Fired after opening a new Neo-tree window. Called with +|neo-tree-window-event-args|. + +"neo_tree_window_before_close"~ +Fired before closing a Neo-tree window. Called with +|neo-tree-window-event-args|. + +"neo_tree_window_after_close"~ +Fired after closing a Neo-tree window. Called with +|neo-tree-window-event-args|. + +NOTE: The following events are used internally and not intended for end user +usage. You can use them if you want, but beware that they may be debounced, and +the details of how frequently they are fired and what events are dropped will be +changed without warning. + +"vim_diagnostic_changed"~ +Fired on the |DiagnosticChanged| autocmd event. The arg is a table with one +property: `diagnostics_lookup`, which is a table where the keys are file names +and the values are tables with diagnostic counts by severity level. + +"vim_buffer_changed"~ +Fired on the following autocmd events: |BufDelete|, |BufWritePost|, +|BufFilePost|, |BufNew| + +"vim_buffer_enter"~ +Fired on the following autocmd events: |BufEnter|, |BufWinEnter| + +"vim_dir_changed"~ +Fired on the |DirChanged| autocmd event + +"vim_win_enter"~ +Fired on the |WinEnter| autocmd event + +"vim_colorscheme"~ +Fired on the |ColorScheme| autocmd event + + +You can also define your own with: +> +>lua + require("neo-tree.events.queue").define_event(event_name, { + setup = , + seed = , + teardown = , + debounce_frequency = , + once = , + cancelled = + }) +< + +The setup function is run the first time the event is subscribed to. For an +autocmd event, this would define the vim autocmd to connect it to fire_event(). + +The `seed` function is run at the beginning of every event firing. The diagnostics +event uses this to collect the diagnostic information and pass it to all +subscribers. + +The `teardown` function is used when the last subscriber unsubscribes, and cleans +up. This is like Dispose in other languages. + +`debounce_frequency` is the minimum number of milliseconds between each invocation +of the event. The first event is guaranteed to fire, as well as the last one, but +in between events may be dropped if this is set to a number greater than zero. + +`once` means to only fire this event handler once then mark it as `cancelled`. + +`cancelled` means that this event handler will be skipped in all future event +fires, and will be discarded on the next cleanup of the queue. + + +COMPONENTS AND RENDERERS *neo-tree-renderers* + +A renderer is just a list of component configs, to be rendered in order to +create a line in the tree. Each renderer is for a specific node type, such as +`directory` or `file`. To view the available built-in components and their +configs for each source, look at the default config by pasting it with +>vim + :lua require("neo-tree").paste_default_config() +< +or view it online at: +https://github.com/nvim-neo-tree/neo-tree.nvim/blob/v3.x/lua/neo-tree/defaults.lua + +A default `renderers` config is specified at the root level and will be used +by each source unless another renderer is defined. If you just want to +rearrange or remove components, you can do so by changing these `renderers` +configs. + + *neo-tree-components* +A component is a function that returns a single text object: +>lua + { + text = "Node A", + highlight = "Normal" + } +< + +... or a list of text objects: +>lua + { + { + text = "Node Name", + highlight = "Directory" + }, + { + text = "[", + highlight = "Comment" + }, + { + text = "I'm Special!", + highlight = "SpecialChar" + }, + text = "[", + highlight = "Comment" + } + } +< + +The only reason to return a list of objects is to use multiple highlight groups. +These components and renderers are defined per source by passing them in the +setup. If you define a component with the same name of a built-in component, it +will replace that built-in component. Otherwise it will be added to the existing +set of components. + +CONTAINER *neo-tree-container* + +One unique component that deserves some explanation is the `container` +component. This component allows you to create more complex layouts where +components can overlap, have a specific size, or be right aligned. A container +has the following properties: + +width~ +Width can be specified as a number, meaning actual number of characters, a +string containing a percentage such as `"100%"`, or the special string +`"fit_content"`. The percentage value means percentage of remaining space in +the window. If a window is 40 columns wide, and the rendered content for the +node so far equals 15 characters, then 100% would evaluate to 25 characters. + +The `"fit_content"` value means that it will be the width of the largest +layer. See `zindex` for details about layers. + +If the current position is "current", meaning it is being displayed in a split +instead of as a sidebar, the available width will be calculated as the longest +node name + indent + 8 characters. This is to prevent right aligned components +from being too far away from the node name. + +min_width / max_width~ +This constrains the value of width, useful when the `width` is set to a +percentage or `"fit_content"`. + +content~ +This is a list of components that will be arranged by this container. + +Each component in the content list can use these additional properties: + + zindex~ + All components with the same zindex will be rendered together in the same + layer, one after the other. Higher zindex value are rendered on top of other + layers, hiding whatever is beneath them. For example, if a component with a + zindex of 10 produces this: +> + "abcdefg" +< + and another component width a zindex of 20 produces this: +> + "1234" +< + then the result will be: +> + "1234efg" +< + + align~ + If align is right, then it will be pushed to the right edge of the available + space. This makes the most sense when the container width is set to a number + or `"100%"`. Components that are right aligned will automatically overlap left + aligned components with the same zindex if there is not enough space. + + Continuing with the example from above, if there was a `"right"` aligned + component with a zindex of 20 that outputs: +> + "**"" +< + Then the result when a container has a width of 12 would be: +> + "1234efg **" +< + but if the width was 8 then the result would be: +> + "1234ef**" +< + +Example~ + +This example container has the name on the left hand side, and the +diagnostics and git status aligned to the right hand side of the window. The +diagnostics and git_status will always be shown, and the name will be clipped if +there is not enough space: +>lua + { + "container", + width = "100%", + right_padding = 1, + content = { + { + "name", + use_git_status_colors = true, + zindex = 10 + }, + { "diagnostics", zindex = 20, align = "right" }, + { "git_status", zindex = 20, align = "right" }, + }, + } +< + + +CUSTOM COMPONENTS + +Each component function is called with the following args: + `config` The config object defined in the renderer. This is how a component + can be made to be configurable. This is useful if you want different behavior + in a directory renderer vs a file renderer. The config is a combination of any + options specified in the default_component_configs + (|neo-tree-default-component-configs|), which can be overridden by settings + specified within each renderer config. + + `node` The NuiNode object for this node. The properties can vary by source, but + each one will generally have at least id and name properties. + + `state` This is the state of the plugin. This object is persistent for the + life of the source, with one state object per source per tab. the entirety of + all state and source level configuration is in this one object. Aside from + configuration, it can also hold anything you may want to set in a + "before_render" event. + +For example, here is the simplest possible component: + +>lua + require("neo-tree").setup({ + filesystem = { + components = { + + name = function(config, node) + return { + text = node.name, + highlight = "NeoTreeFileName" + } + end + + } + } + }) +< + +For a more complete example, here is the actual built-in `name` component, which +is much more dynamic and configurable: + +>lua + require("neo-tree").setup({ + filesystem = { + components = { + + name = function(config, node, state) + local highlight = config.highlight or highlights.FILE_NAME + if node.type == "directory" then + highlight = highlights.DIRECTORY_NAME + end + if node:get_depth() == 1 then + highlight = highlights.ROOT_NAME + else + if config.use_git_status_colors == nil or config.use_git_status_colors then + local git_status = state.components.git_status({}, node, state) + if git_status and git_status.highlight then + highlight = git_status.highlight + end + end + end + return { + text = node.name, + highlight = highlight, + } + end + + } + } + }) +< + + +BUFFER VARIABLES *neo-tree-buffer-variables* + +Neo-tree sets certain buffer options and variables that you may use in custom +code or integrations if you need it. The |filetype| of the main window is +`neo-tree`. The buffer will also have these local variables set: + +`winid` The window handle of the window that it was created in. +`tabid` The id of the tab that it was created in. +`tabnr` (deprecated) The number of the tab that it was created in. +`source` The name of the source that created it, i.e. filesystem, buffers, etc. + +Please note that if the buffer is displayed in another window or tab, it's +behavior is unpredictable. It is meant to be locked to it's original location, +which is why those variables are recorded. + + +POPUPS *neo-tree-popups* + +Popups will be created with a |filetype| of `neo-tree-popup`. You can use this +as the target for autocmds or to exclude them from being acted upon by other +plugins. + +They can also be configured by setting the `popup_border_style` in your config. +To modify the title bar text that appears on a popup window, use +the `window.popup.title` config option to define a function mapping the window +state to a string. The colors of the popup border are controlled by the +`NeoTreeFloatBorder` highlight group.If you you use the special `NC` option for +`popup_border_style`, the title bar of that popup uses the `NeoTreeTitleBar` +highlight group. + + +================================================================================ +OTHER SOURCES ~ +================================================================================ + *neo-tree-sources* + +Neo-tree supports other sources beside the filesystem source which is used by +default. The rest of the sources follow the same pattern as the filesystem +sources described above. The following sections will give an overview of each +source and describe the options that are unique to those sources. + + +BUFFERS *neo-tree-buffers* + +The buffers source shows all open buffers. This is the same list that |ls| would +show. This view adds one component, which is the buffer number, shown to the +right of the file name by default. + +If you use sessions, your previously loaded buffers may be saved as part of +the session, but they will be unloaded at first. If you want to see these +unloaded buffers, set `show_unloaded = true` in your `buffers` config. +Otherwise, you will only see the buffers that have been opened since starting +nvim. + +As a list of files, this source shares most of the commands with the filesystem +source, with the exception of filtering. Some of these commands make less +sense to use here, as things like adding new files won't be visible until you +open them by some other means. One command that is unique to this view is +`buffer_delete`, which issues |:bdelete| on the selected buffer. This is mapped +to `bd` by default. + + +GIT STATUS *neo-tree-git-status-source* + +The git_status view shows the output of the `git status` command in the tree. +Unlike the other sources, this will always show the project root of the +current working directory. If the working tree is clean, this view will be +empty. + +This view has most file commands except for "add", plus the following git +specific commands: +>lua + ["A"] = "git_add_all", + ["ga"] = "git_add_file", + ["gu"] = "git_unstage_file", + ["gU"] = "git_undo_last_commit", + ["gr"] = "git_revert_file", + ["gc"] = "git_commit" + ["gp"] = "git_push", + ["gg"] = "git_commit_and_push", +< + +DOCUMENT SYMBOLS *neo-tree-document-symbols* + +The document_symbols source lists the symbols in the current document obtained +by the LSP request "textDocument/documentSymbols". + +Its configuration includes the following options: + +follow_cursor~ +If set to `true`, will automatically focus on the symbol under the cursor. + +kinds~ +A table specifying how LSP kinds should be rendered. Each entry should map the +LSP kind name to an icon and a highlight group, for example + `Class = { icon = "󰌗", hl = "Include" }` + +custom_kinds~ +A table mapping the LSP kind id (an integer) to the LSP kind name that is used +for `kinds`, for example + `[252] = 'TypeAlias'` + +For the list of kinds (id and name), please refer to +https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol + +client_filters~ +This option could be used to set which LSP server is used to obtain the document +symbols. This accepts one of the following values + + `"first"`: use the first LSP server that provides the feature + `"all"`: use all LSP server that provides the feature + `{ fn = function(name), allow_only = table, ignore = table }` where + `fn`: a function that returns `true` if the server `name` should be used + `allow_only`: use only servers from this list + `ignore`: exclude all servers from this list + NOTE: `fn` preceeds `allow_only` preceeds `ignore` + +For example: (NOTE: here only `fn` will be taken into account) +>lua + { + fn = function(name) return name ~= "null-ls" end, + allow_only = { "clangd", "lua_ls" }, + ignore = { "pyright" }, + } +< +Currently, this source supports the following commands: +>lua + ["o"] = "jump_to_symbol", + ["r"] = "rename", + ["P"] = "preview", (and related commands) + ["s"] = "split", (and related commands) +< +vim:tw=80:ts=2:et:ft=help: diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree.lua new file mode 100644 index 0000000..2491533 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree.lua @@ -0,0 +1,124 @@ +local M = {} + +--- To be removed in a future release, use this instead: +--- ```lua +--- require("neo-tree.command").execute({ action = "close" }) +--- ``` +---@deprecated +M.close_all = function() + require("neo-tree.command").execute({ action = "close" }) +end + +---@type neotree.Config? +local new_user_config = nil + +---Updates the config of neo-tree using the latest user config passed through setup, if any. +---@return neotree.Config.Base +M.ensure_config = function() + if not M.config or new_user_config then + M.config = require("neo-tree.setup").merge_config(new_user_config) + new_user_config = nil + end + return M.config +end + +---A performance-focused version for checking a specific key of a config while trying not to do expensive setup work +---@return neotree.Config.Base +M.peek_config = function() + return new_user_config or M.ensure_config() +end + +---@param ignore_filetypes string[]? +---@param ignore_winfixbuf boolean? +M.get_prior_window = function(ignore_filetypes, ignore_winfixbuf) + local utils = require("neo-tree.utils") + ignore_filetypes = ignore_filetypes or {} + local ignore = utils.list_to_dict(ignore_filetypes) + ignore["neo-tree"] = true + + local tabid = vim.api.nvim_get_current_tabpage() + local wins = utils.prior_windows[tabid] + if wins == nil then + return -1 + end + local win_index = #wins + while win_index > 0 do + local last_win = wins[win_index] + if type(last_win) == "number" then + local success, is_valid = pcall(vim.api.nvim_win_is_valid, last_win) + if success and is_valid and not (ignore_winfixbuf and utils.is_winfixbuf(last_win)) then + local buf = vim.api.nvim_win_get_buf(last_win) + local ft = vim.bo[buf].filetype + local bt = vim.bo[buf].buftype or "normal" + if ignore[ft] ~= true and ignore[bt] ~= true then + return last_win + end + end + end + win_index = win_index - 1 + end + return -1 +end + +M.paste_default_config = function() + local utils = require("neo-tree.utils") + ---@type string + local base_path = assert(debug.getinfo(utils.truthy).source:match("@(.*)/utils/init.lua$")) + ---@type string + local config_path = base_path .. utils.path_separator .. "defaults.lua" + ---@type string[]? + local lines = vim.fn.readfile(config_path) + if lines == nil then + error("Could not read neo-tree.defaults") + end + + -- read up to the end of the config, jut to omit the final return + ---@type string[] + local config = {} + for _, line in ipairs(lines) do + table.insert(config, line) + if line == "}" then + break + end + end + + vim.api.nvim_put(config, "l", true, false) + vim.schedule(function() + vim.cmd("normal! `[v`]=") + end) +end + +M.set_log_level = function(level) + require("neo-tree.log").set_level(level) +end + +---Ideally this should only be in plugin/neo-tree.lua but lazy-loading might mean this runs before bufenter +---@param path string? The path to check +---@return boolean hijacked Whether we hijacked a buffer +local function try_netrw_hijack(path) + if not path or #path == 0 then + return false + end + + local stats = (vim.uv or vim.loop).fs_stat(path) + if not stats or stats.type ~= "directory" then + return false + end + + return require("neo-tree.setup.netrw").hijack() +end + +---@param config neotree.Config +M.setup = function(config) + -- merging is deferred until ensure_config + new_user_config = config + if vim.v.vim_did_enter == 0 then + try_netrw_hijack(vim.fn.argv(0) --[[@as string]]) + end +end + +M.show_logs = function() + vim.cmd("tabnew " .. require("neo-tree.log").outfile) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/collections.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/collections.lua new file mode 100644 index 0000000..5f8b940 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/collections.lua @@ -0,0 +1,146 @@ +local log = require("neo-tree.log") + +---@class neotree.collections.ListNode +---@field prev neotree.collections.ListNode? +---@field next neotree.collections.ListNode? +---@field value any + +local Node = {} +function Node:new(value) + local props = { prev = nil, next = nil, value = value } + setmetatable(props, self) + self.__index = self + return props +end + +---@class neotree.collections.LinkedList +---@field head neotree.collections.ListNode? +---@field tail neotree.collections.ListNode? +---@field size integer +local LinkedList = {} + +---@return neotree.collections.LinkedList +function LinkedList:new() + local props = { head = nil, tail = nil, size = 0 } + setmetatable(props, self) + self.__index = self + return props +end + +---@param node neotree.collections.ListNode +function LinkedList:add_node(node) + if self.head == nil then + self.head = node + self.tail = node + else + self.tail.next = node + node.prev = self.tail + self.tail = node + end + self.size = self.size + 1 + return node +end + +---@param node neotree.collections.ListNode +function LinkedList:remove_node(node) + if node.prev ~= nil then + node.prev.next = node.next + end + if node.next ~= nil then + node.next.prev = node.prev + end + if self.head == node then + self.head = node.next + end + if self.tail == node then + self.tail = node.prev + end + self.size = self.size - 1 + node.prev = nil + node.next = nil + node.value = nil +end + +-- First in Last Out +---@class neotree.collections.Queue +---@field _list neotree.collections.LinkedList +local Queue = {} + +---@return neotree.collections.Queue +function Queue:new() + local props = { _list = LinkedList:new() } + setmetatable(props, self) + self.__index = self + return props +end + +---Add an element to the end of the queue. +---@param value any The value to add. +function Queue:add(value) + self._list:add_node(Node:new(value)) +end + +---Iterates over the entire list, running func(value) on each element. +---If func returns true, the element is removed from the list. +---@param func function The function to run on each element. +---@return table? result +function Queue:for_each(func) + local node = self._list.head + while node ~= nil do + local result = func(node.value) + local node_is_next = false + if result then + if type(result) == "boolean" then + local node_to_remove = node + node = node.next + node_is_next = true + self._list:remove_node(node_to_remove) + elseif type(result) == "table" then + if result.handled == true then + log.trace( + "Handler ", + node.value.id, + " for " + .. node.value.event + .. " returned handled = true, skipping the rest of the queue." + ) + return result + end + end + end + if not node_is_next then + ---@diagnostic disable-next-line: need-check-nil + node = node.next + end + end +end + +function Queue:is_empty() + return self._list.size == 0 +end + +function Queue:remove_by_id(id) + local current = self._list.head + while current ~= nil do + local is_match = false + local item = current.value + if item ~= nil then + local item_id = item.id or item + if item_id == id then + is_match = true + end + end + if is_match then + local next = current.next + self._list:remove_node(current) + current = next + else + current = current.next + end + end +end + +return { + Queue = Queue, + LinkedList = LinkedList, +} diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/command/completion.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/command/completion.lua new file mode 100644 index 0000000..93e217f --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/command/completion.lua @@ -0,0 +1,130 @@ +local parser = require("neo-tree.command.parser") +local utils = require("neo-tree.utils") + +local M = { + show_key_value_completions = true, +} + +---@param key_prefix string? +---@param base_path string +---@return string paths_string +local get_path_completions = function(key_prefix, base_path) + key_prefix = key_prefix or "" + local completions = {} + local expanded = parser.resolve_path(base_path) + local path_completions = vim.fn.glob(expanded .. "*", false, true) + for _, completion in ipairs(path_completions) do + if expanded ~= base_path then + -- we need to recreate the relative path from the aboluste path + -- first strip trailing slashes to normalize + if expanded:sub(-1) == utils.path_separator then + expanded = expanded:sub(1, -2) + end + if base_path:sub(-1) == utils.path_separator then + base_path = base_path:sub(1, -2) + end + -- now put just the current completion onto the base_path being used + completion = base_path .. string.sub(completion, #expanded + 1) + end + table.insert(completions, key_prefix .. completion) + end + + return table.concat(completions, "\n") +end + +---@param key_prefix string? +---@return string references_string +local get_ref_completions = function(key_prefix) + key_prefix = key_prefix or "" + local completions = { key_prefix .. "HEAD" } + local ok, refs = utils.execute_command("git show-ref") + if not ok then + return "" + end + for _, ref in ipairs(refs) do + local _, i = ref:find("refs%/%a+%/") + if i then + table.insert(completions, key_prefix .. ref:sub(i + 1)) + end + end + + return table.concat(completions, "\n") +end + +---@param argLead string +---@param cmdLine string +---@return string candidates_string +M.complete_args = function(argLead, cmdLine) + local candidates = {} + local existing = utils.split(cmdLine, " ") + local parsed = parser.parse(existing, false) + + local eq = string.find(argLead, "=") + if eq == nil then + if M.show_key_value_completions then + -- may be the start of a new key=value pair + for _, key in ipairs(parser.list_args) do + key = tostring(key) + if key:find(argLead, 1, true) and not parsed[key] then + table.insert(candidates, key .. "=") + end + end + + for _, key in ipairs(parser.path_args) do + key = tostring(key) + if key:find(argLead, 1, true) and not parsed[key] then + table.insert(candidates, key .. "=./") + end + end + + for _, key in ipairs(parser.ref_args) do + key = tostring(key) + if key:find(argLead, 1, true) and not parsed[key] then + table.insert(candidates, key .. "=") + end + end + end + else + -- continuation of a key=value pair + local key = string.sub(argLead, 1, eq - 1) + local value = string.sub(argLead, eq + 1) + local arg_type = parser.argtype_lookup[key] + if arg_type == parser.argtypes.PATH then + return get_path_completions(key .. "=", value) + elseif arg_type == parser.argtypes.REF then + return get_ref_completions(key .. "=") + elseif arg_type == parser.argtypes.LIST then + local valid_values = parser.arguments[key].values + if valid_values and not (parsed[key] and #parsed[key] > 0) then + for _, vv in ipairs(valid_values) do + if vv:find(value, 1, true) then + table.insert(candidates, key .. "=" .. vv) + end + end + end + end + end + + -- may be a value without a key + for value, key in pairs(parser.reverse_lookup) do + value = tostring(value) + local key_already_used = false + if parser.argtype_lookup[key] == parser.argtypes.LIST then + key_already_used = type(parsed[key]) ~= "nil" + else + key_already_used = type(parsed[value]) ~= "nil" + end + + if not key_already_used and value:find(argLead, 1, true) then + table.insert(candidates, value) + end + end + + if #candidates == 0 then + -- default to path completion + return get_path_completions(nil, argLead) .. "\n" .. get_ref_completions(nil) + end + return table.concat(candidates, "\n") +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/command/init.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/command/init.lua new file mode 100644 index 0000000..0dfe2e0 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/command/init.lua @@ -0,0 +1,245 @@ +local parser = require("neo-tree.command.parser") +local log = require("neo-tree.log") +local manager = require("neo-tree.sources.manager") +local utils = require("neo-tree.utils") +local renderer = require("neo-tree.ui.renderer") +local inputs = require("neo-tree.ui.inputs") +local completion = require("neo-tree.command.completion") +local do_show_or_focus, handle_reveal + +local M = { + complete_args = completion.complete_args, +} + +-- Store the last source used for `M.execute` +M._last = { + source = nil, + position = nil, +} + +---Executes a Neo-tree action from outside of a Neo-tree window, +---such as show, hide, navigate, etc. +---@param args table The action to execute. The table can have the following keys: +--- action = string The action to execute, can be one of: +--- "close", +--- "focus", <-- default value +--- "show", +--- source = string The source to use for this action. This will default +--- to the default_source specified in the user's config. +--- Can be one of: +--- "filesystem", +--- "buffers", +--- "git_status", +-- "migrations" +--- position = string The position this action will affect. This will default +--- to the the last used position or the position specified +--- in the user's config for the given source. Can be one of: +--- "left", +--- "right", +--- "float", +--- "current" +--- toggle = boolean Whether to toggle the visibility of the Neo-tree window. +--- reveal = boolean Whether to reveal the current file in the Neo-tree window. +--- reveal_file = string The specific file to reveal. +--- dir = string The root directory to set. +--- git_base = string The git base used for diff +M.execute = function(args) + local nt = require("neo-tree") + nt.ensure_config() + + if args.source == "migrations" then + require("neo-tree.setup.deprecations").show_migrations() + return + end + + args.action = args.action or "focus" + + -- handle close action, which can specify a source and/or position + if args.action == "close" then + if args.source then + manager.close(args.source, args.position) + else + manager.close_all(args.position) + end + return + end + + -- The rest of the actions require a source + args.source = args.source or nt.config.default_source + + -- Handle source=last + if args.source == "last" then + args.source = M._last.source or nt.config.default_source + + -- Restore last position if it was not specified + if args.position == nil then + args.position = M._last.position + end + + -- Prevent the default source from being set to "last" + if args.source == "last" then + args.source = nt.config.sources[1] + end + end + M._last.source = args.source + M._last.position = args.position + + -- If position=current was requested, but we are currently in a neo-tree window, + -- then we need to override that. + if args.position == "current" and vim.bo.filetype == "neo-tree" then + local position = vim.api.nvim_buf_get_var(0, "neo_tree_position") + if position then + args.position = position + end + end + + -- Now get the correct state + ---@type neotree.State + local state + local requested_position = args.position or nt.config[args.source].window.position + if requested_position == "current" then + local winid = vim.api.nvim_get_current_win() + state = manager.get_state(args.source, nil, winid) + else + state = manager.get_state(args.source, nil, nil) + end + + -- Next handle toggle, the rest is irrelevant if there is a window to toggle + if args.toggle then + if renderer.close(state) then + -- It was open, and now it's not. + return + end + end + + -- Handle position override + local default_position = nt.config[args.source].window.position + local current_position = state.current_position or default_position + local position_changed = false + if args.position then + state.current_position = args.position + position_changed = args.position ~= current_position + end + + -- Handle setting directory if requested + local path_changed = false + if utils.truthy(args.dir) then + -- Root paths on Windows have 3 characters ("C:\") + local root_len = vim.fn.has("win32") == 1 and 3 or 1 + if #args.dir > root_len and args.dir:sub(-1) == utils.path_separator then + args.dir = args.dir:sub(1, -2) + end + path_changed = state.path ~= args.dir + end + + -- Handle setting git ref + local git_base_changed = state.git_base ~= args.git_base + if utils.truthy(args.git_base) then + state.git_base = args.git_base + end + + -- Handle source selector option + state.enable_source_selector = args.selector + + -- Handle reveal logic + args.reveal = args.reveal or args.reveal_force_cwd + local do_reveal = utils.truthy(args.reveal_file) + if args.reveal and not do_reveal then + args.reveal_file = manager.get_path_to_reveal() + do_reveal = utils.truthy(args.reveal_file) + end + + -- All set, now show or focus the window + local force_navigate = path_changed or do_reveal or git_base_changed or state.dirty + --if position_changed and args.position ~= "current" and current_position ~= "current" then + -- manager.close(args.source) + --end + if do_reveal then + handle_reveal(args, state) + return + end + if not args.dir then + args.dir = state.path + end + do_show_or_focus(args, state, force_navigate) +end + +---Parses and executes the command line. Use execute(args) instead. +---@param ... string Argument as strings. +M._command = function(...) + local args = parser.parse({ ... }, true) + M.execute(args) +end + +do_show_or_focus = function(args, state, force_navigate) + local window_exists = renderer.window_exists(state) + local function close_other_sources() + if not window_exists then + -- Clear the space in case another source is already open + local target_position = args.position or state.current_position or state.window.position + if target_position ~= "current" then + manager.close_all(target_position) + end + end + end + + if args.action == "show" then + -- "show" means show the window without focusing it + if window_exists and not force_navigate then + -- There's nothing to do here, we are already at the target state + return + end + -- close_other_sources() + local current_win = vim.api.nvim_get_current_win() + manager.navigate(state, args.dir, args.reveal_file, function() + -- navigate changes the window to neo-tree, so just quickly hop back to the original window + vim.api.nvim_set_current_win(current_win) + end, false) + elseif args.action == "focus" then + -- "focus" mean open and jump to the window if closed, and just focus it if already opened + if window_exists then + vim.api.nvim_set_current_win(state.winid) + end + if force_navigate or not window_exists then + -- close_other_sources() + manager.navigate(state, args.dir, args.reveal_file, nil, false) + end + end +end + +handle_reveal = function(args, state) + args.reveal_file = utils.normalize_path(args.reveal_file) + -- Deal with cwd if we need to + local cwd = args.dir or state.path or manager.get_cwd(state) + if utils.is_subpath(cwd, args.reveal_file) then + args.dir = cwd + do_show_or_focus(args, state, true) + return + end + + local reveal_file_parent, _ = utils.split_path(args.reveal_file) --[[@as string]] + if args.reveal_force_cwd then + args.dir = reveal_file_parent + do_show_or_focus(args, state, true) + return + end + + -- if dir doesn't have the reveal_file, ignore the reveal_file + if args.dir then + args.reveal_file = nil + do_show_or_focus(args, state, true) + return + end + + -- force was not specified and the file does not belong to cwd, so we need to ask the user + inputs.confirm("File not in cwd. Change cwd to " .. reveal_file_parent .. "?", function(response) + if response == true then + args.dir = reveal_file_parent + else + args.reveal_file = nil + end + do_show_or_focus(args, state, true) + end) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/command/parser.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/command/parser.lua new file mode 100644 index 0000000..05b8ff5 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/command/parser.lua @@ -0,0 +1,208 @@ +local uv = vim.uv or vim.loop +local utils = require("neo-tree.utils") +local _compat = require("neo-tree.utils._compat") + +---@enum neotree.command.ParserArgument.Type +local argtype = { + FLAG = "", + LIST = "", + PATH = "", + REF = "", +} + +---@class neotree.command.Parser +---@field argtypes table +local M = { + argtypes = argtype, +} + +---@param all_source_names string[] +M.setup = function(all_source_names) + local source_names = vim.deepcopy(all_source_names, _compat.noref()) + table.insert(source_names, "migrations") + + -- A special source referring to the last used source. + table.insert(source_names, "last") + + ---@class neotree.command.ParserArgument + ---@field type neotree.command.ParserArgument.Type + + -- For lists, the first value is the default value. + ---@class neotree.command.ParserArguments + ---@field [string] neotree.command.ParserArgument + ---@field values string[] + local arguments = { + action = { + type = M.argtypes.LIST, + values = { + "close", + "focus", + "show", + }, + }, + position = { + type = M.argtypes.LIST, + values = { + "left", + "right", + "top", + "bottom", + "float", + "current", + }, + }, + source = { + type = M.argtypes.LIST, + values = source_names, + }, + dir = { type = M.argtypes.PATH, stat_type = "directory" }, + reveal_file = { type = M.argtypes.PATH, stat_type = "file" }, + git_base = { type = M.argtypes.REF }, + toggle = { type = M.argtypes.FLAG }, + reveal = { type = M.argtypes.FLAG }, + reveal_force_cwd = { type = M.argtypes.FLAG }, + selector = { type = M.argtypes.FLAG }, + } + + local arg_type_lookup = {} + local list_args = {} + local path_args = {} + local ref_args = {} + local flag_args = {} + local reverse_lookup = {} + for name, def in pairs(arguments) do + arg_type_lookup[name] = def.type + if def.type == M.argtypes.LIST then + table.insert(list_args, name) + for _, vv in ipairs(def.values) do + reverse_lookup[tostring(vv)] = name + end + elseif def.type == M.argtypes.PATH then + table.insert(path_args, name) + elseif def.type == M.argtypes.FLAG then + table.insert(flag_args, name) + reverse_lookup[name] = M.argtypes.FLAG + elseif def.type == M.argtypes.REF then + table.insert(ref_args, name) + else + error("Unknown type: " .. def.type) + end + end + + M.arguments = arguments + M.list_args = list_args + M.path_args = path_args + M.ref_args = ref_args + M.flag_args = flag_args + M.argtype_lookup = arg_type_lookup + M.reverse_lookup = reverse_lookup +end + +---@param path string +---@param validate_type string? +M.resolve_path = function(path, validate_type) + path = vim.fs.normalize(path) + local expanded = vim.fn.expand(path) + local abs_path = vim.fn.fnamemodify(expanded, ":p") + if validate_type then + local stat = uv.fs_stat(abs_path) + if not stat or stat.type ~= validate_type then + error("Invalid path: " .. path .. " is not a " .. validate_type) + end + end + return abs_path +end + +---@param ref string +M.verify_git_ref = function(ref) + local ok, _ = utils.execute_command("git rev-parse --verify " .. ref) + return ok +end + +---@class neotree.command.Parser.Parsed +---@field [string] string|boolean + +---@param result neotree.command.Parser.Parsed +---@param arg string +local parse_arg = function(result, arg) + if type(arg) ~= "string" then + return + end + local eq = arg:find("=") + if eq then + local key = arg:sub(1, eq - 1) + local value = arg:sub(eq + 1) + local def = M.arguments[key] + if not def.type then + error("Invalid argument: " .. arg) + end + + if def.type == M.argtypes.PATH then + result[key] = M.resolve_path(value, def.stat_type) + elseif def.type == M.argtypes.FLAG then + if value == "true" then + result[key] = true + elseif value == "false" then + result[key] = false + else + error("Invalid value for " .. key .. ": " .. value) + end + elseif def.type == M.argtypes.REF then + if not M.verify_git_ref(value) then + error("Invalid value for " .. key .. ": " .. value) + end + result[key] = value + else + result[key] = value + end + else + local value = arg + local key = M.reverse_lookup[value] + if key == nil then + -- maybe it's a git ref + if M.verify_git_ref(value) then + result["git_base"] = value + return + end + -- maybe it's a path + local path = M.resolve_path(value) + local stat = uv.fs_stat(path) + if stat then + if stat.type == "directory" then + result["dir"] = path + elseif stat.type == "file" then + result["reveal_file"] = path + end + else + error("Invalid argument: " .. arg) + end + elseif key == M.argtypes.FLAG then + result[value] = true + else + result[key] = value + end + end +end + +---@param args string|string[] +---@param strict_checking boolean +---@return neotree.command.Parser.Parsed parsed_args +M.parse = function(args, strict_checking) + require("neo-tree").ensure_config() + local result = {} + + if type(args) == "string" then + args = utils.split(args, " ") + end + -- read args from user + for _, arg in ipairs(args) do + local success, err = pcall(parse_arg, result, arg) + if strict_checking and not success then + error(err) + end + end + + return result +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/defaults.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/defaults.lua new file mode 100644 index 0000000..81d76a5 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/defaults.lua @@ -0,0 +1,745 @@ +---@type neotree.Config.Base +local config = { + -- If a user has a sources list it will replace this one. + -- Only sources listed here will be loaded. + -- You can also add an external source by adding it's name to this list. + -- The name used here must be the same name you would use in a require() call. + sources = { + "filesystem", + "buffers", + "git_status", + -- "document_symbols", + }, + add_blank_line_at_top = false, -- Add a blank line at the top of the tree. + auto_clean_after_session_restore = false, -- Automatically clean up broken neo-tree buffers saved in sessions + close_if_last_window = false, -- Close Neo-tree if it is the last window left in the tab + default_source = "filesystem", -- you can choose a specific source `last` here which indicates the last used source + enable_diagnostics = true, + enable_git_status = true, + enable_modified_markers = true, -- Show markers for files with unsaved changes. + enable_opened_markers = true, -- Enable tracking of opened files. Required for `components.name.highlight_opened_files` + enable_refresh_on_write = true, -- Refresh the tree when a file is written. Only used if `use_libuv_file_watcher` is false. + enable_cursor_hijack = false, -- If enabled neotree will keep the cursor on the first letter of the filename when moving in the tree. + git_status_async = true, + -- These options are for people with VERY large git repos + git_status_async_options = { + batch_size = 1000, -- how many lines of git status results to process at a time + batch_delay = 10, -- delay in ms between batches. Spreads out the workload to let other processes run. + max_lines = 10000, -- How many lines of git status results to process. Anything after this will be dropped. + -- Anything before this will be used. The last items to be processed are the untracked files. + }, + hide_root_node = false, -- Hide the root node. + retain_hidden_root_indent = false, -- IF the root node is hidden, keep the indentation anyhow. + -- This is needed if you use expanders because they render in the indent. + log_level = "info", -- "trace", "debug", "info", "warn", "error", "fatal" + log_to_file = false, -- true, false, "/path/to/file.log", use ':lua require("neo-tree").show_logs()' to show the file + open_files_in_last_window = true, -- false = open files in top left window + open_files_do_not_replace_types = { "terminal", "Trouble", "qf", "edgy" }, -- when opening files, do not use windows containing these filetypes or buftypes + open_files_using_relative_paths = false, + -- popup_border_style is for input and confirmation dialogs. + -- Configurtaion of floating window is done in the individual source sections. + -- "NC" is a special style that works well with NormalNC set + popup_border_style = "NC", -- "double", "rounded", "single", "solid", (or "" to use 'winborder' on Neovim v0.11+) + resize_timer_interval = 500, -- in ms, needed for containers to redraw right aligned and faded content + -- set to -1 to disable the resize timer entirely + -- -- NOTE: this will speed up to 50 ms for 1 second following a resize + sort_case_insensitive = false, -- used when sorting files and directories in the tree + sort_function = nil , -- uses a custom function for sorting files and directories in the tree + use_popups_for_input = true, -- If false, inputs will use vim.ui.input() instead of custom floats. + use_default_mappings = true, + -- source_selector provides clickable tabs to switch between sources. + source_selector = { + winbar = false, -- toggle to show selector on winbar + statusline = false, -- toggle to show selector on statusline + show_scrolled_off_parent_node = false, -- this will replace the tabs with the parent path + -- of the top visible node when scrolled down. + sources = { + { source = "filesystem" }, + { source = "buffers" }, + { source = "git_status" }, + }, + content_layout = "start", -- only with `tabs_layout` = "equal", "focus" + -- start : |/ 󰓩 bufname \/... + -- end : |/ 󰓩 bufname \/... + -- center : |/ 󰓩 bufname \/... + tabs_layout = "equal", -- start, end, center, equal, focus + -- start : |/ a \/ b \/ c \ | + -- end : | / a \/ b \/ c \| + -- center : | / a \/ b \/ c \ | + -- equal : |/ a \/ b \/ c \| + -- active : |/ focused tab \/ b \/ c \| + truncation_character = "…", -- character to use when truncating the tab label + tabs_min_width = nil, -- nil | int: if int padding is added based on `content_layout` + tabs_max_width = nil, -- this will truncate text even if `text_trunc_to_fit = false` + padding = 0, -- can be int or table + -- padding = { left = 2, right = 0 }, + -- separator = "▕", -- can be string or table, see below + separator = { left = "▏", right= "▕" }, + -- separator = { left = "/", right = "\\", override = nil }, -- |/ a \/ b \/ c \... + -- separator = { left = "/", right = "\\", override = "right" }, -- |/ a \ b \ c \... + -- separator = { left = "/", right = "\\", override = "left" }, -- |/ a / b / c /... + -- separator = { left = "/", right = "\\", override = "active" },-- |/ a / b:active \ c \... + -- separator = "|", -- || a | b | c |... + separator_active = nil, -- set separators around the active tab. nil falls back to `source_selector.separator` + show_separator_on_edge = false, + -- true : |/ a \/ b \/ c \| + -- false : | a \/ b \/ c | + highlight_tab = "NeoTreeTabInactive", + highlight_tab_active = "NeoTreeTabActive", + highlight_background = "NeoTreeTabInactive", + highlight_separator = "NeoTreeTabSeparatorInactive", + highlight_separator_active = "NeoTreeTabSeparatorActive", + }, + -- + --event_handlers = { + -- { + -- event = "before_render", + -- handler = function (state) + -- -- add something to the state that can be used by custom components + -- end + -- }, + -- { + -- event = "file_opened", + -- handler = function(file_path) + -- --auto close + -- require("neo-tree.command").execute({ action = "close" }) + -- end + -- }, + -- { + -- event = "file_opened", + -- handler = function(file_path) + -- --clear search after opening a file + -- require("neo-tree.sources.filesystem").reset_search() + -- end + -- }, + -- { + -- event = "file_renamed", + -- handler = function(args) + -- -- fix references to file + -- print(args.source, " renamed to ", args.destination) + -- end + -- }, + -- { + -- event = "file_moved", + -- handler = function(args) + -- -- fix references to file + -- print(args.source, " moved to ", args.destination) + -- end + -- }, + -- { + -- event = "neo_tree_buffer_enter", + -- handler = function() + -- vim.cmd 'highlight! Cursor blend=100' + -- end + -- }, + -- { + -- event = "neo_tree_buffer_leave", + -- handler = function() + -- vim.cmd 'highlight! Cursor guibg=#5f87af blend=0' + -- end + -- }, + -- { + -- event = "neo_tree_window_before_open", + -- handler = function(args) + -- print("neo_tree_window_before_open", vim.inspect(args)) + -- end + -- }, + -- { + -- event = "neo_tree_window_after_open", + -- handler = function(args) + -- vim.cmd("wincmd =") + -- end + -- }, + -- { + -- event = "neo_tree_window_before_close", + -- handler = function(args) + -- print("neo_tree_window_before_close", vim.inspect(args)) + -- end + -- }, + -- { + -- event = "neo_tree_window_after_close", + -- handler = function(args) + -- vim.cmd("wincmd =") + -- end + -- } + --}, + default_component_configs = { + container = { + enable_character_fade = true, + width = "100%", + right_padding = 0, + }, + --diagnostics = { + -- symbols = { + -- hint = "H", + -- info = "I", + -- warn = "!", + -- error = "X", + -- }, + -- highlights = { + -- hint = "DiagnosticSignHint", + -- info = "DiagnosticSignInfo", + -- warn = "DiagnosticSignWarn", + -- error = "DiagnosticSignError", + -- }, + --}, + indent = { + indent_size = 2, + padding = 1, + -- indent guides + with_markers = true, + indent_marker = "│", + last_indent_marker = "└", + highlight = "NeoTreeIndentMarker", + -- expander config, needed for nesting files + with_expanders = nil, -- if nil and file nesting is enabled, will enable expanders + expander_collapsed = "", + expander_expanded = "", + expander_highlight = "NeoTreeExpander", + }, + icon = { + folder_closed = "", + folder_open = "", + folder_empty = "󰉖", + folder_empty_open = "󰷏", + -- The next two settings are only a fallback, if you use nvim-web-devicons and configure default icons there + -- then these will never be used. + default = "*", + highlight = "NeoTreeFileIcon", + provider = function(icon, node, state) -- default icon provider utilizes nvim-web-devicons if available + if node.type == "file" or node.type == "terminal" then + local success, web_devicons = pcall(require, "nvim-web-devicons") + local name = node.type == "terminal" and "terminal" or node.name + if success then + local devicon, hl = web_devicons.get_icon(name) + icon.text = devicon or icon.text + icon.highlight = hl or icon.highlight + end + end + end + }, + modified = { + symbol = "[+] ", + highlight = "NeoTreeModified", + }, + name = { + trailing_slash = false, + highlight_opened_files = false, -- Requires `enable_opened_markers = true`. + -- Take values in { false (no highlight), true (only loaded), + -- "all" (both loaded and unloaded)}. For more information, + -- see the `show_unloaded` config of the `buffers` source. + use_git_status_colors = true, + highlight = "NeoTreeFileName", + }, + git_status = { + symbols = { + -- Change type + added = "✚", -- NOTE: you can set any of these to an empty string to not show them + deleted = "✖", + modified = "", + renamed = "󰁕", + -- Status type + untracked = "", + ignored = "", + unstaged = "󰄱", + staged = "", + conflict = "", + }, + align = "right", + }, + -- If you don't want to use these columns, you can set `enabled = false` for each of them individually + file_size = { + enabled = true, + width = 12, -- width of the column + required_width = 64, -- min width of window required to show this column + }, + type = { + enabled = true, + width = 10, -- width of the column + required_width = 110, -- min width of window required to show this column + }, + last_modified = { + enabled = true, + width = 20, -- width of the column + required_width = 88, -- min width of window required to show this column + format = "%Y-%m-%d %I:%M %p", -- format string for timestamp (see `:h os.date()`) + -- or use a function that takes in the date in seconds and returns a string to display + --format = require("neo-tree.utils").relative_date, -- enable relative timestamps + }, + created = { + enabled = false, + width = 20, -- width of the column + required_width = 120, -- min width of window required to show this column + format = "%Y-%m-%d %I:%M %p", -- format string for timestamp (see `:h os.date()`) + -- or use a function that takes in the date in seconds and returns a string to display + --format = require("neo-tree.utils").relative_date, -- enable relative timestamps + }, + symlink_target = { + enabled = false, + text_format = " ➛ %s", -- %s will be replaced with the symlink target's path. + }, + }, + renderers = { + directory = { + { "indent" }, + { "icon" }, + { "current_filter" }, + { + "container", + content = { + { "name", zindex = 10 }, + { + "symlink_target", + zindex = 10, + highlight = "NeoTreeSymbolicLinkTarget", + }, + { "clipboard", zindex = 10 }, + { "diagnostics", errors_only = true, zindex = 20, align = "right", hide_when_expanded = true }, + { "git_status", zindex = 10, align = "right", hide_when_expanded = true }, + { "file_size", zindex = 10, align = "right" }, + { "type", zindex = 10, align = "right" }, + { "last_modified", zindex = 10, align = "right" }, + { "created", zindex = 10, align = "right" }, + }, + }, + }, + file = { + { "indent" }, + { "icon" }, + { + "container", + content = { + { + "name", + zindex = 10 + }, + { + "symlink_target", + zindex = 10, + highlight = "NeoTreeSymbolicLinkTarget", + }, + { "clipboard", zindex = 10 }, + { "bufnr", zindex = 10 }, + { "modified", zindex = 20, align = "right" }, + { "diagnostics", zindex = 20, align = "right" }, + { "git_status", zindex = 10, align = "right" }, + { "file_size", zindex = 10, align = "right" }, + { "type", zindex = 10, align = "right" }, + { "last_modified", zindex = 10, align = "right" }, + { "created", zindex = 10, align = "right" }, + }, + }, + }, + message = { + { "indent", with_markers = false }, + { "name", highlight = "NeoTreeMessage" }, + }, + terminal = { + { "indent" }, + { "icon" }, + { "name" }, + { "bufnr" } + } + }, + nesting_rules = {}, + -- Global custom commands that will be available in all sources (if not overridden in `opts[source_name].commands`) + -- + -- You can then reference the custom command by adding a mapping to it: + -- globally -> `opts.window.mappings` + -- locally -> `opt[source_name].window.mappings` to make it source specific. + -- + -- commands = { | window { | filesystem { + -- hello = function() | mappings = { | commands = { + -- print("Hello world") | [""] = "hello" | hello = function() + -- end | } | print("Hello world in filesystem") + -- } | } | end + -- + -- see `:h neo-tree-custom-commands-global` + commands = {}, -- A list of functions + + window = { -- see https://github.com/MunifTanjim/nui.nvim/tree/main/lua/nui/popup for + -- possible options. These can also be functions that return these options. + position = "left", -- left, right, top, bottom, float, current + width = 40, -- applies to left and right positions + height = 15, -- applies to top and bottom positions + auto_expand_width = false, -- expand the window when file exceeds the window width. does not work with position = "float" + popup = { -- settings that apply to float position only + size = { + height = "80%", + width = "50%", + }, + position = "50%", -- 50% means center it + title = function (state) -- format the text that appears at the top of a popup window + return "Neo-tree " .. state.name:gsub("^%l", string.upper) + end, + -- you can also specify border here, if you want a different setting from + -- the global popup_border_style. + }, + insert_as = "child", -- Affects how nodes get inserted into the tree during creation/pasting/moving of files if the node under the cursor is a directory: + -- "child": Insert nodes as children of the directory under cursor. + -- "sibling": Insert nodes as siblings of the directory under cursor. + -- Mappings for tree window. See `:h neo-tree-mappings` for a list of built-in commands. + -- You can also create your own commands by providing a function instead of a string. + mapping_options = { + noremap = true, + nowait = true, + }, + mappings = { + [""] = { + "toggle_node", + nowait = false, -- disable `nowait` if you have existing combos starting with this char that you want to use + }, + ["<2-LeftMouse>"] = "open", + [""] = "open", + -- [""] = { "open", config = { expand_nested_files = true } }, -- expand nested file takes precedence + [""] = "cancel", -- close preview or floating neo-tree window + ["P"] = { + "toggle_preview", + config = { + use_float = true, + use_snacks_image = true, + use_image_nvim = true, + -- title = "Neo-tree Preview", -- You can define a custom title for the preview floating window. + } + }, + [""] = { "scroll_preview", config = {direction = -10} }, + [""] = { "scroll_preview", config = {direction = 10} }, + ["l"] = "focus_preview", + ["S"] = "open_split", + -- ["S"] = "split_with_window_picker", + ["s"] = "open_vsplit", + -- ["sr"] = "open_rightbelow_vs", + -- ["sl"] = "open_leftabove_vs", + -- ["s"] = "vsplit_with_window_picker", + ["t"] = "open_tabnew", + -- [""] = "open_drop", + -- ["t"] = "open_tab_drop", + ["w"] = "open_with_window_picker", + ["C"] = "close_node", + --["C"] = "close_all_subnodes", + ["z"] = "close_all_nodes", + --["Z"] = "expand_all_nodes", + --["Z"] = "expand_all_subnodes", + ["R"] = "refresh", + ["a"] = { + "add", + -- some commands may take optional config options, see `:h neo-tree-mappings` for details + config = { + show_path = "none", -- "none", "relative", "absolute" + } + }, + ["A"] = "add_directory", -- also accepts the config.show_path and config.insert_as options. + ["d"] = "delete", + ["r"] = "rename", + ["y"] = "copy_to_clipboard", + ["x"] = "cut_to_clipboard", + ["p"] = "paste_from_clipboard", + ["c"] = "copy", -- takes text input for destination, also accepts the config.show_path and config.insert_as options + ["m"] = "move", -- takes text input for destination, also accepts the config.show_path and config.insert_as options + ["e"] = "toggle_auto_expand_width", + ["q"] = "close_window", + ["?"] = "show_help", + ["<"] = "prev_source", + [">"] = "next_source", + }, + }, + filesystem = { + window = { + mappings = { + ["H"] = "toggle_hidden", + ["/"] = "fuzzy_finder", + --["/"] = {"fuzzy_finder", config = { keep_filter_on_submit = true }}, + --["/"] = "filter_as_you_type", -- this was the default until v1.28 + ["D"] = "fuzzy_finder_directory", + -- ["D"] = "fuzzy_sorter_directory", + ["#"] = "fuzzy_sorter", -- fuzzy sorting using the fzy algorithm + ["f"] = "filter_on_submit", + [""] = "clear_filter", + [""] = "navigate_up", + ["."] = "set_root", + ["[g"] = "prev_git_modified", + ["]g"] = "next_git_modified", + ["i"] = "show_file_details", -- see `:h neo-tree-file-actions` for options to customize the window. + ["b"] = "rename_basename", + ["o"] = { "show_help", nowait=false, config = { title = "Order by", prefix_key = "o" }}, + ["oc"] = { "order_by_created", nowait = false }, + ["od"] = { "order_by_diagnostics", nowait = false }, + ["og"] = { "order_by_git_status", nowait = false }, + ["om"] = { "order_by_modified", nowait = false }, + ["on"] = { "order_by_name", nowait = false }, + ["os"] = { "order_by_size", nowait = false }, + ["ot"] = { "order_by_type", nowait = false }, + }, + fuzzy_finder_mappings = { -- define keymaps for filter popup window in fuzzy_finder_mode + [""] = "move_cursor_down", + [""] = "move_cursor_down", + [""] = "move_cursor_up", + [""] = "move_cursor_up", + [""] = "close", + [""] = "close_keep_filter", + [""] = "close_clear_filter", + [""] = { "", raw = true }, + { + -- normal mode mappings + n = { + ["j"] = "move_cursor_down", + ["k"] = "move_cursor_up", + [""] = "close_keep_filter", + [""] = "close_clear_filter", + [""] = "close", + } + } + -- [""] = "noop", -- if you want to use normal mode + -- ["key"] = function(state, scroll_padding) ... end, + }, + }, + async_directory_scan = "auto", -- "auto" means refreshes are async, but it's synchronous when called from the Neotree commands. + -- "always" means directory scans are always async. + -- "never" means directory scans are never async. + scan_mode = "shallow", -- "shallow": Don't scan into directories to detect possible empty directory a priori + -- "deep": Scan into directories to detect empty or grouped empty directories a priori. + bind_to_cwd = true, -- true creates a 2-way binding between vim's cwd and neo-tree's root + cwd_target = { + sidebar = "tab", -- sidebar is when position = left or right + current = "window" -- current is when position = current + }, + check_gitignore_in_search = true, -- check gitignore status for files/directories when searching + -- setting this to false will speed up searches, but gitignored + -- items won't be marked if they are visible. + -- The renderer section provides the renderers that will be used to render the tree. + -- The first level is the node type. + -- For each node type, you can specify a list of components to render. + -- Components are rendered in the order they are specified. + -- The first field in each component is the name of the function to call. + -- The rest of the fields are passed to the function as the "config" argument. + filtered_items = { + visible = false, -- when true, they will just be displayed differently than normal items + force_visible_in_empty_folder = false, -- when true, hidden files will be shown if the root folder is otherwise empty + children_inherit_highlights = true, -- whether children of filtered parents should inherit their parent's highlight group + show_hidden_count = true, -- when true, the number of hidden items in each folder will be shown as the last entry + hide_dotfiles = true, + hide_gitignored = true, + hide_hidden = true, -- only works on Windows for hidden files/directories + hide_by_name = { + ".DS_Store", + "thumbs.db" + --"node_modules", + }, + hide_by_pattern = { -- uses glob style patterns + --"*.meta", + --"*/src/*/tsconfig.json" + }, + always_show = { -- remains visible even if other settings would normally hide it + --".gitignored", + }, + always_show_by_pattern = { -- uses glob style patterns + --".env*", + }, + never_show = { -- remains hidden even if visible is toggled to true, this overrides always_show + --".DS_Store", + --"thumbs.db" + }, + never_show_by_pattern = { -- uses glob style patterns + --".null-ls_*", + }, + }, + find_by_full_path_words = false, -- `false` means it only searches the tail of a path. + -- `true` will change the filter into a full path + -- search with space as an implicit ".*", so + -- `fi init` + -- will match: `./sources/filesystem/init.lua + --find_command = "fd", -- this is determined automatically, you probably don't need to set it + --find_args = { -- you can specify extra args to pass to the find command. + -- fd = { + -- "--exclude", ".git", + -- "--exclude", "node_modules" + -- } + --}, + ---- or use a function instead of list of strings + --find_args = function(cmd, path, search_term, args) + -- if cmd ~= "fd" then + -- return args + -- end + -- --maybe you want to force the filter to always include hidden files: + -- table.insert(args, "--hidden") + -- -- but no one ever wants to see .git files + -- table.insert(args, "--exclude") + -- table.insert(args, ".git") + -- -- or node_modules + -- table.insert(args, "--exclude") + -- table.insert(args, "node_modules") + -- --here is where it pays to use the function, you can exclude more for + -- --short search terms, or vary based on the directory + -- if string.len(search_term) < 4 and path == "/home/cseickel" then + -- table.insert(args, "--exclude") + -- table.insert(args, "Library") + -- end + -- return args + --end, + group_empty_dirs = false, -- when true, empty folders will be grouped together + search_limit = 50, -- max number of search results when using filters + follow_current_file = { + enabled = false, -- This will find and focus the file in the active buffer every time + -- -- the current file is changed while the tree is open. + leave_dirs_open = false, -- `false` closes auto expanded dirs, such as with `:Neotree reveal` + }, + hijack_netrw_behavior = "open_default", -- netrw disabled, opening a directory opens neo-tree + -- in whatever position is specified in window.position + -- "open_current",-- netrw disabled, opening a directory opens within the + -- window like netrw would, regardless of window.position + -- "disabled", -- netrw left alone, neo-tree does not handle opening dirs + use_libuv_file_watcher = false, -- This will use the OS level file watchers to detect changes + -- instead of relying on nvim autocmd events. + }, + buffers = { + bind_to_cwd = true, + follow_current_file = { + enabled = true, -- This will find and focus the file in the active buffer every time + -- -- the current file is changed while the tree is open. + leave_dirs_open = false, -- `false` closes auto expanded dirs, such as with `:Neotree reveal` + }, + group_empty_dirs = true, -- when true, empty directories will be grouped together + show_unloaded = false, -- When working with sessions, for example, restored but unfocused buffers + -- are mark as "unloaded". Turn this on to view these unloaded buffer. + terminals_first = false, -- when true, terminals will be listed before file buffers + window = { + mappings = { + [""] = "navigate_up", + ["."] = "set_root", + ["d"] = "buffer_delete", + ["bd"] = "buffer_delete", + ["i"] = "show_file_details", -- see `:h neo-tree-file-actions` for options to customize the window. + ["b"] = "rename_basename", + ["o"] = { "show_help", nowait=false, config = { title = "Order by", prefix_key = "o" }}, + ["oc"] = { "order_by_created", nowait = false }, + ["od"] = { "order_by_diagnostics", nowait = false }, + ["om"] = { "order_by_modified", nowait = false }, + ["on"] = { "order_by_name", nowait = false }, + ["os"] = { "order_by_size", nowait = false }, + ["ot"] = { "order_by_type", nowait = false }, + }, + }, + }, + git_status = { + window = { + mappings = { + ["A"] = "git_add_all", + ["gu"] = "git_unstage_file", + ["gU"] = "git_undo_last_commit", + ["ga"] = "git_add_file", + ["gr"] = "git_revert_file", + ["gc"] = "git_commit", + ["gp"] = "git_push", + ["gg"] = "git_commit_and_push", + ["i"] = "show_file_details", -- see `:h neo-tree-file-actions` for options to customize the window. + ["b"] = "rename_basename", + ["o"] = { "show_help", nowait=false, config = { title = "Order by", prefix_key = "o" }}, + ["oc"] = { "order_by_created", nowait = false }, + ["od"] = { "order_by_diagnostics", nowait = false }, + ["om"] = { "order_by_modified", nowait = false }, + ["on"] = { "order_by_name", nowait = false }, + ["os"] = { "order_by_size", nowait = false }, + ["ot"] = { "order_by_type", nowait = false }, + }, + }, + }, + document_symbols = { + follow_cursor = false, + client_filters = "first", + renderers = { + root = { + {"indent"}, + {"icon", default="C" }, + {"name", zindex = 10}, + }, + symbol = { + {"indent", with_expanders = true}, + {"kind_icon", default="?" }, + {"container", + content = { + {"name", zindex = 10}, + {"kind_name", zindex = 20, align = "right"}, + } + } + }, + }, + window = { + mappings = { + [""] = "jump_to_symbol", + ["o"] = "jump_to_symbol", + ["A"] = "noop", -- also accepts the config.show_path and config.insert_as options. + ["d"] = "noop", + ["y"] = "noop", + ["x"] = "noop", + ["p"] = "noop", + ["c"] = "noop", + ["m"] = "noop", + ["a"] = "noop", + ["/"] = "filter", + ["f"] = "filter_on_submit", + }, + }, + custom_kinds = { + -- define custom kinds here (also remember to add icon and hl group to kinds) + -- ccls + -- [252] = 'TypeAlias', + -- [253] = 'Parameter', + -- [254] = 'StaticMethod', + -- [255] = 'Macro', + }, + kinds = { + Unknown = { icon = "?", hl = "" }, + Root = { icon = "", hl = "NeoTreeRootName" }, + File = { icon = "󰈙", hl = "Tag" }, + Module = { icon = "", hl = "Exception" }, + Namespace = { icon = "󰌗", hl = "Include" }, + Package = { icon = "󰏖", hl = "Label" }, + Class = { icon = "󰌗", hl = "Include" }, + Method = { icon = "", hl = "Function" }, + Property = { icon = "󰆧", hl = "@property" }, + Field = { icon = "", hl = "@field" }, + Constructor = { icon = "", hl = "@constructor" }, + Enum = { icon = "󰒻", hl = "@number" }, + Interface = { icon = "", hl = "Type" }, + Function = { icon = "󰊕", hl = "Function" }, + Variable = { icon = "", hl = "@variable" }, + Constant = { icon = "", hl = "Constant" }, + String = { icon = "󰀬", hl = "String" }, + Number = { icon = "󰎠", hl = "Number" }, + Boolean = { icon = "", hl = "Boolean" }, + Array = { icon = "󰅪", hl = "Type" }, + Object = { icon = "󰅩", hl = "Type" }, + Key = { icon = "󰌋", hl = "" }, + Null = { icon = "", hl = "Constant" }, + EnumMember = { icon = "", hl = "Number" }, + Struct = { icon = "󰌗", hl = "Type" }, + Event = { icon = "", hl = "Constant" }, + Operator = { icon = "󰆕", hl = "Operator" }, + TypeParameter = { icon = "󰊄", hl = "Type" }, + + -- ccls + -- TypeAlias = { icon = ' ', hl = 'Type' }, + -- Parameter = { icon = ' ', hl = '@parameter' }, + -- StaticMethod = { icon = '󰠄 ', hl = 'Function' }, + -- Macro = { icon = ' ', hl = 'Macro' }, + } + }, + example = { + renderers = { + custom = { + {"indent"}, + {"icon", default="C" }, + {"custom"}, + {"name"} + } + }, + window = { + mappings = { + [""] = "toggle_node", + [""] = "example_command", + ["d"] = "show_debug_info", + }, + }, + }, +} +return config diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/events/init.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/events/init.lua new file mode 100644 index 0000000..0db3ada --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/events/init.lua @@ -0,0 +1,110 @@ +local q = require("neo-tree.events.queue") +local log = require("neo-tree.log") +local utils = require("neo-tree.utils") + +---@class neotree.event.Functions +local M = { + -- Well known event names, you can make up your own + AFTER_RENDER = "after_render", + BEFORE_FILE_ADD = "before_file_add", + BEFORE_FILE_DELETE = "before_file_delete", + BEFORE_FILE_MOVE = "before_file_move", + BEFORE_FILE_RENAME = "before_file_rename", + BEFORE_RENDER = "before_render", + FILE_ADDED = "file_added", + FILE_DELETED = "file_deleted", + FILE_MOVED = "file_moved", + FILE_OPENED = "file_opened", + FILE_OPEN_REQUESTED = "file_open_requested", + FILE_RENAMED = "file_renamed", + FS_EVENT = "fs_event", + GIT_EVENT = "git_event", + GIT_STATUS_CHANGED = "git_status_changed", + STATE_CREATED = "state_created", + NEO_TREE_BUFFER_ENTER = "neo_tree_buffer_enter", + NEO_TREE_BUFFER_LEAVE = "neo_tree_buffer_leave", + NEO_TREE_LSP_UPDATE = "neo_tree_lsp_update", + NEO_TREE_POPUP_BUFFER_ENTER = "neo_tree_popup_buffer_enter", + NEO_TREE_POPUP_BUFFER_LEAVE = "neo_tree_popup_buffer_leave", + NEO_TREE_POPUP_INPUT_READY = "neo_tree_popup_input_ready", + NEO_TREE_WINDOW_AFTER_CLOSE = "neo_tree_window_after_close", + NEO_TREE_WINDOW_AFTER_OPEN = "neo_tree_window_after_open", + NEO_TREE_WINDOW_BEFORE_CLOSE = "neo_tree_window_before_close", + NEO_TREE_WINDOW_BEFORE_OPEN = "neo_tree_window_before_open", + NEO_TREE_PREVIEW_BEFORE_RENDER = "neo_tree_preview_before_render", + VIM_AFTER_SESSION_LOAD = "vim_after_session_load", + VIM_BUFFER_ADDED = "vim_buffer_added", + VIM_BUFFER_CHANGED = "vim_buffer_changed", + VIM_BUFFER_DELETED = "vim_buffer_deleted", + VIM_BUFFER_ENTER = "vim_buffer_enter", + VIM_BUFFER_MODIFIED_SET = "vim_buffer_modified_set", + VIM_COLORSCHEME = "vim_colorscheme", + VIM_CURSOR_MOVED = "vim_cursor_moved", + VIM_DIAGNOSTIC_CHANGED = "vim_diagnostic_changed", + VIM_DIR_CHANGED = "vim_dir_changed", + VIM_INSERT_LEAVE = "vim_insert_leave", + VIM_LEAVE = "vim_leave", + VIM_LSP_REQUEST = "vim_lsp_request", + VIM_RESIZED = "vim_resized", + VIM_TAB_CLOSED = "vim_tab_closed", + VIM_TERMINAL_ENTER = "vim_terminal_enter", + VIM_TEXT_CHANGED_NORMAL = "vim_text_changed_normal", + VIM_WIN_CLOSED = "vim_win_closed", + VIM_WIN_ENTER = "vim_win_enter", +} + +---@param autocmds string +---@return string event +---@return string? pattern +local parse_autocmd_string = function(autocmds) + local parsed = vim.split(autocmds, " ") + return parsed[1], parsed[2] +end + +---@param event_name neotree.EventName|string +---@param autocmds string[] +---@param debounce_frequency integer? +---@param seed_fn function? +---@param nested boolean? +M.define_autocmd_event = function(event_name, autocmds, debounce_frequency, seed_fn, nested) + log.debug("Defining autocmd event: %s", event_name) + local augroup_name = "NeoTreeEvent_" .. event_name + q.define_event(event_name, { + setup = function() + local augroup = vim.api.nvim_create_augroup(augroup_name, { clear = false }) + for _, autocmd in ipairs(autocmds) do + local event, pattern = parse_autocmd_string(autocmd) + log.trace("Registering autocmds on %s %s", event, pattern or "") + vim.api.nvim_create_autocmd({ event }, { + pattern = pattern or "*", + group = augroup, + nested = nested, + callback = function(args) + ---@class neotree.event.Autocmd.CallbackArgs : neotree._vim.api.keyset.create_autocmd.callback_args + ---@field afile string + local event_args = args --[[@as neotree._vim.api.keyset.create_autocmd.callback_args]] + event_args.afile = args.file or "" + M.fire_event(event_name, event_args) + end, + }) + end + end, + seed = seed_fn, + teardown = function() + log.trace("Teardown autocmds for ", event_name) + vim.api.nvim_create_augroup(augroup_name, { clear = true }) + end, + debounce_frequency = debounce_frequency, + debounce_strategy = utils.debounce_strategy.CALL_LAST_ONLY, + }) +end + +M.clear_all_events = q.clear_all_events +M.define_event = q.define_event +M.destroy_event = q.destroy_event +M.fire_event = q.fire_event + +M.subscribe = q.subscribe +M.unsubscribe = q.unsubscribe + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/events/queue.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/events/queue.lua new file mode 100644 index 0000000..db32beb --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/events/queue.lua @@ -0,0 +1,167 @@ +local utils = require("neo-tree.utils") +local log = require("neo-tree.log") +local Queue = require("neo-tree.collections").Queue + +---@type table +local event_queues = {} +---@type table +local event_definitions = {} +local M = {} + +---@class neotree.event.Handler.Result +---@field handled boolean? + +---@class neotree.event.Handler +---@field event neotree.EventName|string +---@field handler fun(table?):(neotree.event.Handler.Result?) +---@field id string? + +local typecheck = require("neo-tree.health.typecheck") +local validate = typecheck.validate +---@param event_handler neotree.event.Handler +local validate_event_handler = function(event_handler) + return validate("event_handler", event_handler, function(eh) + validate("event", eh.event, "string") + validate("handler", eh.handler, "function") + end) +end + +M.clear_all_events = function() + for event_name, queue in pairs(event_queues) do + M.destroy_event(event_name) + end + event_queues = {} +end + +---@class neotree.event.Definition +---@field teardown function? +---@field setup function? +---@field setup_was_run boolean? + +---@param event_name neotree.EventName|string +---@param opts neotree.event.Definition +M.define_event = function(event_name, opts) + local existing = event_definitions[event_name] + if existing ~= nil then + error("Event already defined: " .. event_name) + end + event_definitions[event_name] = opts +end + +---@param event_name neotree.EventName|string +---@return boolean existed_and_destroyed +M.destroy_event = function(event_name) + local existing = event_definitions[event_name] + if existing == nil then + return false + end + if existing.setup_was_run and type(existing.teardown) == "function" then + local success, result = pcall(existing.teardown) + if not success then + error("Error in teardown for " .. event_name .. ": " .. result) + end + existing.setup_was_run = false + end + event_queues[event_name] = nil + return true +end + +---@param event neotree.EventName|string +---@param args table +local fire_event_internal = function(event, args) + local queue = event_queues[event] + if queue == nil then + return nil + end + --log.trace("Firing event: ", event, " with args: ", args) + + if queue:is_empty() then + --log.trace("Event queue is empty") + return nil + end + local seed = utils.get_value(event_definitions, event .. ".seed") + if seed ~= nil then + local success, result = pcall(seed, args) + if success and result then + log.trace("Seed for " .. event .. " returned: " .. tostring(result)) + elseif success then + log.trace("Seed for " .. event .. " returned falsy, cancelling event") + else + log.error("Error in seed function for " .. event .. ": " .. result) + end + end + + return queue:for_each(function(event_handler) + local remove_node = event_handler == nil or event_handler.cancelled + if not remove_node then + local success, result = pcall(event_handler.handler, args) + local id = event_handler.id or event_handler + if success then + log.trace("Handler ", id, " for " .. event .. " called successfully.") + else + log.error(string.format("Error in event handler for event %s[%s]: %s", event, id, result)) + end + if event_handler.once then + event_handler.cancelled = true + return true + end + return result + end + end) +end + +---@param event neotree.EventName|string +---@param args any? +M.fire_event = function(event, args) + local freq = utils.get_value(event_definitions, event .. ".debounce_frequency", 0, true) + local strategy = utils.get_value(event_definitions, event .. ".debounce_strategy", 0, true) + log.trace("Firing event: ", event, " with args: ", args) + if freq > 0 then + utils.debounce("EVENT_FIRED: " .. event, function() + fire_event_internal(event, args or {}) + end, freq, strategy) + else + return fire_event_internal(event, args or {}) + end +end + +---@param event_handler neotree.event.Handler +M.subscribe = function(event_handler) + validate_event_handler(event_handler) + + local queue = event_queues[event_handler.event] + if queue == nil then + log.debug("Creating queue for event: " .. event_handler.event) + queue = Queue:new() + local def = event_definitions[event_handler.event] + if def and type(def.setup) == "function" then + local success, result = pcall(def.setup) + if success then + def.setup_was_run = true + log.debug("Setup for event " .. event_handler.event .. " was run") + else + log.error("Error in setup for " .. event_handler.event .. ": " .. result) + end + end + event_queues[event_handler.event] = queue + end + log.debug("Adding event handler [", event_handler.id, "] for event: ", event_handler.event) + queue:add(event_handler) +end + +---@param event_handler neotree.event.Handler +M.unsubscribe = function(event_handler) + local queue = event_queues[event_handler.event] + if queue == nil then + return nil + end + queue:remove_by_id(event_handler.id or event_handler) + if queue:is_empty() then + M.destroy_event(event_handler.event) + event_queues[event_handler.event] = nil + else + event_queues[event_handler.event] = queue + end +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/git/ignored.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/git/ignored.lua new file mode 100644 index 0000000..db6c6d5 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/git/ignored.lua @@ -0,0 +1,183 @@ +local Job = require("plenary.job") +local uv = vim.uv or vim.loop + +local utils = require("neo-tree.utils") +local log = require("neo-tree.log") +local git_utils = require("neo-tree.git.utils") + +local M = {} +local sep = utils.path_separator + +---@param ignored string[] +---@param path string +---@param _type neotree.Filetype +M.is_ignored = function(ignored, path, _type) + if _type == "directory" and not utils.is_windows then + path = path .. sep + end + + return vim.tbl_contains(ignored, path) +end + +local git_root_cache = { + known_roots = {}, + dir_lookup = {}, +} +local get_root_for_item = function(item) + local dir = item.type == "directory" and item.path or item.parent_path + if type(git_root_cache.dir_lookup[dir]) ~= "nil" then + return git_root_cache.dir_lookup[dir] + end + --for _, root in ipairs(git_root_cache.known_roots) do + -- if vim.startswith(dir, root) then + -- git_root_cache.dir_lookup[dir] = root + -- return root + -- end + --end + local root = git_utils.get_repository_root(dir) + if root then + git_root_cache.dir_lookup[dir] = root + table.insert(git_root_cache.known_roots, root) + else + git_root_cache.dir_lookup[dir] = false + end + return root +end + +---@param state neotree.State +---@param items neotree.FileItem[] +M.mark_ignored = function(state, items, callback) + local folders = {} + log.trace("================================================================================") + log.trace("IGNORED: mark_ignore BEGIN...") + + for _, item in ipairs(items) do + local folder = utils.split_path(item.path) + if folder then + if not folders[folder] then + folders[folder] = {} + end + table.insert(folders[folder], item.path) + end + end + + local function process_result(result) + if utils.is_windows then + --on Windows, git seems to return quotes and double backslash "path\\directory" + result = vim.tbl_map(function(item) + item = item:gsub("\\\\", "\\") + return item + end, result) + else + --check-ignore does not indicate directories the same as 'status' so we need to + --add the trailing slash to the path manually if not on Windows. + log.trace("IGNORED: Checking types of", #result, "items to see which ones are directories") + for i, item in ipairs(result) do + local stat = uv.fs_stat(item) + if stat and stat.type == "directory" then + result[i] = item .. sep + end + end + end + result = vim.tbl_map(function(item) + -- remove leading and trailing " from git output + item = item:gsub('^"', ""):gsub('"$', "") + -- convert octal encoded lines to utf-8 + item = git_utils.octal_to_utf8(item) + return item + end, result) + return result + end + + local function finalize(all_results) + local show_gitignored = state.filtered_items and state.filtered_items.hide_gitignored == false + log.trace("IGNORED: Comparing results to mark items as ignored:", show_gitignored) + local ignored, not_ignored = 0, 0 + for _, item in ipairs(items) do + if M.is_ignored(all_results, item.path, item.type) then + item.filtered_by = item.filtered_by or {} + item.filtered_by.gitignored = true + item.filtered_by.show_gitignored = show_gitignored + ignored = ignored + 1 + else + not_ignored = not_ignored + 1 + end + end + log.trace("IGNORED: mark_ignored is complete, ignored:", ignored, ", not ignored:", not_ignored) + log.trace("================================================================================") + end + + local all_results = {} + if type(callback) == "function" then + local jobs = {} + local running_jobs = 0 + local job_count = 0 + local completed_jobs = 0 + + -- This is called when a job completes, and starts the next job if there are any left + -- or calls the callback if all jobs are complete. + -- It is also called once at the start to start the first 50 jobs. + -- + -- This is done to avoid running too many jobs at once, which can cause a crash from + -- having too many open files. + local run_more_jobs = function() + while #jobs > 0 and running_jobs < 50 and job_count > completed_jobs do + local next_job = table.remove(jobs, #jobs) + next_job:start() + running_jobs = running_jobs + 1 + end + + if completed_jobs == job_count then + finalize(all_results) + callback(all_results) + end + end + + for folder, folder_items in pairs(folders) do + local args = { "-C", folder, "check-ignore", "--stdin" } + ---@diagnostic disable-next-line: missing-fields + local job = Job:new({ + command = "git", + args = args, + enabled_recording = true, + writer = folder_items, + on_start = function() + log.trace("IGNORED: Running async git with args: ", args) + end, + on_exit = function(self, code, _) + local result + if code ~= 0 then + log.debug("Failed to load ignored files for", folder, ":", self:stderr_result()) + result = {} + else + result = self:result() + end + vim.list_extend(all_results, process_result(result)) + + running_jobs = running_jobs - 1 + completed_jobs = completed_jobs + 1 + run_more_jobs() + end, + }) + table.insert(jobs, job) + job_count = job_count + 1 + end + + run_more_jobs() + else + for folder, folder_items in pairs(folders) do + local cmd = { "git", "-C", folder, "check-ignore", unpack(folder_items) } + log.trace("IGNORED: Running cmd: ", cmd) + local result = vim.fn.systemlist(cmd) + if vim.v.shell_error == 128 then + log.debug("Failed to load ignored files for", state.path, ":", result) + result = {} + end + vim.list_extend(all_results, process_result(result)) + end + finalize(all_results) + return all_results + end +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/git/init.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/git/init.lua new file mode 100644 index 0000000..863599b --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/git/init.lua @@ -0,0 +1,13 @@ +local status = require("neo-tree.git.status") +local ignored = require("neo-tree.git.ignored") +local git_utils = require("neo-tree.git.utils") + +local M = { + get_repository_root = git_utils.get_repository_root, + is_ignored = ignored.is_ignored, + mark_ignored = ignored.mark_ignored, + status = status.status, + status_async = status.status_async, +} + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/git/status.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/git/status.lua new file mode 100644 index 0000000..74fb29c --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/git/status.lua @@ -0,0 +1,350 @@ +local utils = require("neo-tree.utils") +local events = require("neo-tree.events") +local Job = require("plenary.job") +local log = require("neo-tree.log") +local git_utils = require("neo-tree.git.utils") + +local M = {} + +local function get_simple_git_status_code(status) + -- Prioritze M then A over all others + if status:match("U") or status == "AA" or status == "DD" then + return "U" + elseif status:match("M") then + return "M" + elseif status:match("[ACR]") then + return "A" + elseif status:match("!$") then + return "!" + elseif status:match("?$") then + return "?" + else + local len = #status + while len > 0 do + local char = status:sub(len, len) + if char ~= " " then + return char + end + len = len - 1 + end + return status + end +end + +local function get_priority_git_status_code(status, other_status) + if not status then + return other_status + elseif not other_status then + return status + elseif status == "U" or other_status == "U" then + return "U" + elseif status == "?" or other_status == "?" then + return "?" + elseif status == "M" or other_status == "M" then + return "M" + elseif status == "A" or other_status == "A" then + return "A" + else + return status + end +end + +---@class (exact) neotree.git.Context +---@field git_status neotree.git.Status +---@field git_root string +---@field exclude_directories boolean +---@field lines_parsed integer + +---@alias neotree.git.Status table + +---@param context neotree.git.Context +local parse_git_status_line = function(context, line) + context.lines_parsed = context.lines_parsed + 1 + if type(line) ~= "string" then + return + end + if #line < 3 then + return + end + local git_root = context.git_root + local git_status = context.git_status + local exclude_directories = context.exclude_directories + + local line_parts = vim.split(line, " ") + if #line_parts < 2 then + return + end + local status = line_parts[1] + local relative_path = line_parts[2] + + -- rename output is `R000 from/filename to/filename` + if status:match("^R") then + relative_path = line_parts[3] + end + + -- remove any " due to whitespace or utf-8 in the path + relative_path = relative_path:gsub('^"', ""):gsub('"$', "") + -- convert octal encoded lines to utf-8 + relative_path = git_utils.octal_to_utf8(relative_path) + + if utils.is_windows == true then + relative_path = utils.windowize_path(relative_path) + end + local absolute_path = utils.path_join(git_root, relative_path) + -- merge status result if there are results from multiple passes + local existing_status = git_status[absolute_path] + if existing_status then + local merged = "" + local i = 0 + while i < 2 do + i = i + 1 + local existing_char = #existing_status >= i and existing_status:sub(i, i) or "" + local new_char = #status >= i and status:sub(i, i) or "" + local merged_char = get_priority_git_status_code(existing_char, new_char) + merged = merged .. merged_char + end + status = merged + end + git_status[absolute_path] = status + + if not exclude_directories then + -- Now bubble this status up to the parent directories + local parts = utils.split(absolute_path, utils.path_separator) + table.remove(parts) -- pop the last part so we don't override the file's status + utils.reduce(parts, "", function(acc, part) + local path = acc .. utils.path_separator .. part + if utils.is_windows == true then + path = path:gsub("^" .. utils.path_separator, "") + end + local path_status = git_status[path] + local file_status = get_simple_git_status_code(status) + git_status[path] = get_priority_git_status_code(path_status, file_status) + return path + end) + end +end +---Parse "git status" output for the current working directory. +---@base git ref base +---@exclude_directories boolean Whether to skip bubling up status to directories +---@path string Path to run the git status command in, defaults to cwd. +---@return neotree.git.Status, string? git_status the neotree.Git.Status of the given root +M.status = function(base, exclude_directories, path) + local git_root = git_utils.get_repository_root(path) + if not utils.truthy(git_root) then + return {} + end + + local C = git_root + local staged_cmd = { "git", "-C", C, "diff", "--staged", "--name-status", base, "--" } + local staged_ok, staged_result = utils.execute_command(staged_cmd) + if not staged_ok then + return {} + end + local unstaged_cmd = { "git", "-C", C, "diff", "--name-status" } + local unstaged_ok, unstaged_result = utils.execute_command(unstaged_cmd) + if not unstaged_ok then + return {} + end + local untracked_cmd = { "git", "-C", C, "ls-files", "--exclude-standard", "--others" } + local untracked_ok, untracked_result = utils.execute_command(untracked_cmd) + if not untracked_ok then + return {} + end + + local context = { + git_root = git_root, + git_status = {}, + exclude_directories = exclude_directories, + lines_parsed = 0, + } + + for _, line in ipairs(staged_result) do + parse_git_status_line(context, line) + end + for _, line in ipairs(unstaged_result) do + if line then + line = " " .. line + end + parse_git_status_line(context, line) + end + for _, line in ipairs(untracked_result) do + if line then + line = "? " .. line + end + parse_git_status_line(context, line) + end + + return context.git_status, git_root +end + +local function parse_lines_batch(context, job_complete_callback) + local i, batch_size = 0, context.batch_size + + if context.lines_total == nil then + -- first time through, get the total number of lines + context.lines_total = math.min(context.max_lines, #context.lines) + context.lines_parsed = 0 + if context.lines_total == 0 then + if type(job_complete_callback) == "function" then + job_complete_callback() + end + return + end + end + batch_size = math.min(context.batch_size, context.lines_total - context.lines_parsed) + + while i < batch_size do + i = i + 1 + parse_git_status_line(context, context.lines[context.lines_parsed + 1]) + end + + if context.lines_parsed >= context.lines_total then + if type(job_complete_callback) == "function" then + job_complete_callback() + end + else + -- add small delay so other work can happen + vim.defer_fn(function() + parse_lines_batch(context, job_complete_callback) + end, context.batch_delay) + end +end + +M.status_async = function(path, base, opts) + git_utils.get_repository_root(path, function(git_root) + if utils.truthy(git_root) then + log.trace("git.status.status_async called") + else + log.trace("status_async: not a git folder: ", path) + return false + end + + local event_id = "git_status_" .. git_root + ---@type neotree.git.Context + local context = { + git_root = git_root, + git_status = {}, + exclude_directories = false, + lines = {}, + lines_parsed = 0, + batch_size = opts.batch_size or 1000, + batch_delay = opts.batch_delay or 10, + max_lines = opts.max_lines or 100000, + } + + local should_process = function(err, line, job, err_msg) + if vim.v.dying > 0 or vim.v.exiting ~= vim.NIL then + job:shutdown() + return false + end + if err and err > 0 then + log.error(err_msg, err, line) + return false + end + return true + end + + local job_complete_callback = function() + vim.schedule(function() + events.fire_event(events.GIT_STATUS_CHANGED, { + git_root = context.git_root, + git_status = context.git_status, + }) + end) + end + + local parse_lines = vim.schedule_wrap(function() + parse_lines_batch(context, job_complete_callback) + end) + + utils.debounce(event_id, function() + ---@diagnostic disable-next-line: missing-fields + local staged_job = Job:new({ + command = "git", + args = { "-C", git_root, "diff", "--staged", "--name-status", base, "--" }, + enable_recording = false, + maximium_results = context.max_lines, + on_stdout = function(err, line, job) + if should_process(err, line, job, "status_async staged error:") then + table.insert(context.lines, line) + end + end, + on_stderr = function(err, line) + if err and err > 0 then + log.error("status_async staged error: ", err, line) + end + end, + }) + + ---@diagnostic disable-next-line: missing-fields + local unstaged_job = Job:new({ + command = "git", + args = { "-C", git_root, "diff", "--name-status" }, + enable_recording = false, + maximium_results = context.max_lines, + on_stdout = function(err, line, job) + if should_process(err, line, job, "status_async unstaged error:") then + if line then + line = " " .. line + end + table.insert(context.lines, line) + end + end, + on_stderr = function(err, line) + if err and err > 0 then + log.error("status_async unstaged error: ", err, line) + end + end, + }) + + ---@diagnostic disable-next-line: missing-fields + local untracked_job = Job:new({ + command = "git", + args = { "-C", git_root, "ls-files", "--exclude-standard", "--others" }, + enable_recording = false, + maximium_results = context.max_lines, + on_stdout = function(err, line, job) + if should_process(err, line, job, "status_async untracked error:") then + if line then + line = "? " .. line + end + table.insert(context.lines, line) + end + end, + on_stderr = function(err, line) + if err and err > 0 then + log.error("status_async untracked error: ", err, line) + end + end, + }) + + ---@diagnostic disable-next-line: missing-fields + Job:new({ + command = "git", + args = { + "-C", + git_root, + "config", + "--get", + "status.showUntrackedFiles", + }, + enabled_recording = true, + on_exit = function(self, _, _) + local result = self:result() + log.debug("git status.showUntrackedFiles =", result[1]) + if result[1] == "no" then + unstaged_job:after(parse_lines) + Job.chain(staged_job, unstaged_job) + else + untracked_job:after(parse_lines) + Job.chain(staged_job, unstaged_job, untracked_job) + end + end, + }):start() + end, 1000, utils.debounce_strategy.CALL_FIRST_AND_LAST, utils.debounce_action.START_ASYNC_JOB) + + return true + end) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/git/utils.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/git/utils.lua new file mode 100644 index 0000000..9abcbbd --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/git/utils.lua @@ -0,0 +1,66 @@ +local Job = require("plenary.job") + +local utils = require("neo-tree.utils") +local log = require("neo-tree.log") + +local M = {} + +M.get_repository_root = function(path, callback) + local args = { "rev-parse", "--show-toplevel" } + if utils.truthy(path) then + args = { "-C", path, "rev-parse", "--show-toplevel" } + end + if type(callback) == "function" then + ---@diagnostic disable-next-line: missing-fields + Job:new({ + command = "git", + args = args, + enabled_recording = true, + on_exit = function(self, code, _) + if code ~= 0 then + log.trace("GIT ROOT ERROR ", self:stderr_result()) + callback(nil) + return + end + local git_root = self:result()[1] + + if utils.is_windows then + git_root = utils.windowize_path(git_root) + end + + log.trace("GIT ROOT for '", path, "' is '", git_root, "'") + callback(git_root) + end, + }):start() + else + local ok, git_output = utils.execute_command({ "git", unpack(args) }) + if not ok then + log.trace("GIT ROOT ERROR ", git_output) + return nil + end + local git_root = git_output[1] + + if utils.is_windows then + git_root = utils.windowize_path(git_root) + end + + log.trace("GIT ROOT for '", path, "' is '", git_root, "'") + return git_root + end +end + +local convert_octal_char = function(octal) + return string.char(tonumber(octal, 8)) +end + +M.octal_to_utf8 = function(text) + -- git uses octal encoding for utf-8 filepaths, convert octal back to utf-8 + local success, converted = pcall(string.gsub, text, "\\([0-7][0-7][0-7])", convert_octal_char) + if success then + return converted + else + return text + end +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/health/init.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/health/init.lua new file mode 100644 index 0000000..f0f88e9 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/health/init.lua @@ -0,0 +1,326 @@ +local typecheck = require("neo-tree.health.typecheck") +local M = {} +local health = vim.health + +local function check_dependencies() + local devicons_ok = pcall(require, "nvim-web-devicons") + if devicons_ok then + health.ok("nvim-web-devicons is installed") + else + health.info("nvim-web-devicons not installed") + end + + local plenary_ok = pcall(require, "plenary") + if plenary_ok then + health.ok("plenary.nvim is installed") + else + health.error("plenary.nvim is not installed") + end + + local nui_ok = pcall(require, "nui.tree") + if nui_ok then + health.ok("nui.nvim is installed") + else + health.error("nui.nvim not installed") + end + + health.info("Optional dependencies for preview image support (only need one):") + -- optional + local snacks_ok = pcall(require, "snacks.image") + if snacks_ok then + health.ok("snacks.image is installed") + else + health.info("nui.nvim not installed") + end + + local image_ok = pcall(require, "image") + if image_ok then + health.ok("image.nvim is installed") + else + health.info("nui.nvim not installed") + end +end + +local validate = typecheck.validate + +---@module "neo-tree.types.config" +---@param config neotree.Config.Base +function M.check_config(config) + ---@type [string, string?][] + local errors = {} + local start = vim.uv.hrtime() + local verbose = vim.o.verbose > 0 + local matched, missed = validate( + "config", + config, + function(cfg) + ---@class neotree.health.Validator.Generators + local v = { + array = function(validator) + ---@generic T + ---@param arr T[] + return function(arr) + for i, val in ipairs(arr) do + validate(("[%d]"):format(i), val, validator) + end + end + end, + literal = function(literals) + return function(value) + return vim.tbl_contains(literals, value), + ("value %s did not match literals %s"):format(value, table.concat(literals, "|")) + end + end, + } + local schema = { + Filesystem = { + ---@param follow_current_file neotree.Config.Filesystem.FollowCurrentFile + FollowCurrentFile = function(follow_current_file) + validate("enabled", follow_current_file.enabled, "boolean", true) + validate("leave_dirs_open", follow_current_file.leave_dirs_open, "boolean", true) + end, + }, + + ---@param window neotree.Config.Window + Window = function(window) + validate("mappings", window.mappings, "table") -- TODO: More specific validation for mappings table + end, + SourceSelector = { + ---@param item neotree.Config.SourceSelector.Item + Item = function(item) + validate("source", item.source, "string") + validate("padding", item.padding, { "number", "table" }, true) -- TODO: More specific validation for padding table + validate("separator", item.separator, { "string", "table" }, true) -- TODO: More specific validation for separator table + end, + ---@param sep neotree.Config.SourceSelector.Separator + Separator = function(sep) + validate("left", sep.left, "string") + validate("right", sep.right, "string") + validate("override", sep.override, v.literal({ "right", "left", "active" }), true) + end, + }, + Renderers = v.array("table"), + } + + if not validate("config", cfg, "table") then + health.error("Config does not exist") + return + end + + validate("sources", cfg.sources, v.array("string"), false) + validate("add_blank_line_at_top", cfg.add_blank_line_at_top, "boolean") + validate("auto_clean_after_session_restore", cfg.auto_clean_after_session_restore, "boolean") + validate("close_if_last_window", cfg.close_if_last_window, "boolean") + validate("default_source", cfg.default_source, "string") + validate("enable_diagnostics", cfg.enable_diagnostics, "boolean") + validate("enable_git_status", cfg.enable_git_status, "boolean") + validate("enable_modified_markers", cfg.enable_modified_markers, "boolean") + validate("enable_opened_markers", cfg.enable_opened_markers, "boolean") + validate("enable_refresh_on_write", cfg.enable_refresh_on_write, "boolean") + validate("enable_cursor_hijack", cfg.enable_cursor_hijack, "boolean") + validate("git_status_async", cfg.git_status_async, "boolean") + validate("git_status_async_options", cfg.git_status_async_options, function(options) + validate("batch_size", options.batch_size, "number") + validate("batch_delay", options.batch_delay, "number") + validate("max_lines", options.max_lines, "number") + end) + validate("hide_root_node", cfg.hide_root_node, "boolean") + validate("retain_hidden_root_indent", cfg.retain_hidden_root_indent, "boolean") + validate( + "log_level", + cfg.log_level, + v.literal({ "trace", "debug", "info", "warn", "error", "fatal" }), + true + ) + validate("log_to_file", cfg.log_to_file, { "boolean", "string" }) + validate("open_files_in_last_window", cfg.open_files_in_last_window, "boolean") + validate( + "open_files_do_not_replace_types", + cfg.open_files_do_not_replace_types, + v.array("string") + ) + validate("open_files_using_relative_paths", cfg.open_files_using_relative_paths, "boolean") + validate( + "popup_border_style", + cfg.popup_border_style, + v.literal({ "NC", "rounded", "single", "solid", "double", "" }) + ) + validate("resize_timer_interval", cfg.resize_timer_interval, "number") + validate("sort_case_insensitive", cfg.sort_case_insensitive, "boolean") + validate("sort_function", cfg.sort_function, "function", true) + validate("use_popups_for_input", cfg.use_popups_for_input, "boolean") + validate("use_default_mappings", cfg.use_default_mappings, "boolean") + validate("source_selector", cfg.source_selector, function(ss) + validate("winbar", ss.winbar, "boolean") + validate("statusline", ss.statusline, "boolean") + validate("show_scrolled_off_parent_node", ss.show_scrolled_off_parent_node, "boolean") + validate("sources", ss.sources, v.array(schema.SourceSelector.Item)) + validate("content_layout", ss.content_layout, v.literal({ "start", "end", "center" })) + validate( + "tabs_layout", + ss.tabs_layout, + v.literal({ "equal", "start", "end", "center", "focus" }) + ) + validate("truncation_character", ss.truncation_character, "string", false) + validate("tabs_min_width", ss.tabs_min_width, "number", true) + validate("tabs_max_width", ss.tabs_max_width, "number", true) + validate("padding", ss.padding, { "number", "table" }) -- TODO: More specific validation for padding table + validate("separator", ss.separator, schema.SourceSelector.Separator) + validate("separator_active", ss.separator_active, schema.SourceSelector.Separator, true) + validate("show_separator_on_edge", ss.show_separator_on_edge, "boolean") + validate("highlight_tab", ss.highlight_tab, "string") + validate("highlight_tab_active", ss.highlight_tab_active, "string") + validate("highlight_background", ss.highlight_background, "string") + validate("highlight_separator", ss.highlight_separator, "string") + validate("highlight_separator_active", ss.highlight_separator_active, "string") + end) + validate("event_handlers", cfg.event_handlers, v.array("table"), true) -- TODO: More specific validation for event handlers + validate("default_component_configs", cfg.default_component_configs, function(defaults) + validate("container", defaults.container, "table") -- TODO: More specific validation + validate("indent", defaults.indent, "table") -- TODO: More specific validation + validate("icon", defaults.icon, "table") -- TODO: More specific validation + validate("modified", defaults.modified, "table") -- TODO: More specific validation + validate("name", defaults.name, "table") -- TODO: More specific validation + validate("git_status", defaults.git_status, "table") -- TODO: More specific validation + validate("file_size", defaults.file_size, "table") -- TODO: More specific validation + validate("type", defaults.type, "table") -- TODO: More specific validation + validate("last_modified", defaults.last_modified, "table") -- TODO: More specific validation + validate("created", defaults.created, "table") -- TODO: More specific validation + validate("symlink_target", defaults.symlink_target, "table") -- TODO: More specific validation + end) + validate("renderers", cfg.renderers, schema.Renderers) + validate("nesting_rules", cfg.nesting_rules, v.array("table"), true) -- TODO: More specific validation for nesting rules + validate("commands", cfg.commands, "table", true) -- TODO: More specific validation for commands + validate("window", cfg.window, function(window) + validate("position", window.position, "string") -- TODO: More specific validation + validate("width", window.width, "number") + validate("height", window.height, "number") + validate("auto_expand_width", window.auto_expand_width, "boolean") + validate("popup", window.popup, function(popup) + validate("title", popup.title, "function") + validate("size", popup.size, function(size) + validate("height", size.height, { "string", "number" }) + validate("width", size.width, { "string", "number" }) + end) + validate( + "border", + popup.border, + v.literal({ "NC", "rounded", "single", "solid", "double", "" }), + true + ) + end) + validate("insert_as", window.insert_as, v.literal({ "child", "sibling" }), true) + validate("mapping_options", window.mapping_options, "table") -- TODO: More specific validation + validate("mappings", window.mappings, v.array("table")) -- TODO: More specific validation for mapping items + end) + + validate("filesystem", cfg.filesystem, function(fs) + validate( + "async_directory_scan", + fs.async_directory_scan, + v.literal({ "auto", "always", "never" }) + ) + validate("scan_mode", fs.scan_mode, v.literal({ "shallow", "deep" })) + validate("bind_to_cwd", fs.bind_to_cwd, "boolean") + validate("cwd_target", fs.cwd_target, function(cwd_target) + validate("sidebar", cwd_target.sidebar, v.literal({ "tab", "window", "global" })) + validate("current", cwd_target.current, v.literal({ "tab", "window", "global" })) + end) + validate("check_gitignore_in_search", fs.check_gitignore_in_search, "boolean") + validate("filtered_items", fs.filtered_items, function(f) + validate("visible", f.visible, "boolean") + validate("force_visible_in_empty_folder", f.force_visible_in_empty_folder, "boolean") + validate("children_inherit_highlights", f.children_inherit_highlights, "boolean") + validate("show_hidden_count", f.show_hidden_count, "boolean") + validate("hide_dotfiles", f.hide_dotfiles, "boolean") + validate("hide_gitignored", f.hide_gitignored, "boolean") + validate("hide_hidden", f.hide_hidden, "boolean") + validate("hide_by_name", f.hide_by_name, v.array("string")) + validate("hide_by_pattern", f.hide_by_pattern, v.array("string")) + validate("always_show", f.always_show, v.array("string")) + validate("always_show_by_pattern", f.always_show_by_pattern, v.array("string")) + validate("never_show", f.never_show, v.array("string")) + validate("never_show_by_pattern", f.never_show_by_pattern, v.array("string")) + end) + validate("find_by_full_path_words", fs.find_by_full_path_words, "boolean") + validate("find_command", fs.find_command, "string", true) + validate("find_args", fs.find_args, { "table", "function" }, true) + validate("group_empty_dirs", fs.group_empty_dirs, "boolean") + validate("search_limit", fs.search_limit, "number") + validate("follow_current_file", fs.follow_current_file, schema.Filesystem.FollowCurrentFile) + validate( + "hijack_netrw_behavior", + fs.hijack_netrw_behavior, + v.literal({ "open_default", "open_current", "disabled" }), + true + ) + validate("use_libuv_file_watcher", fs.use_libuv_file_watcher, "boolean") + validate("renderers", fs.renderers, schema.Renderers) + validate("window", fs.window, function(window) + validate("mappings", window.mappings, "table") -- TODO: More specific validation for mappings table + validate("fuzzy_finder_mappings", window.fuzzy_finder_mappings, "table") -- TODO: More specific validation + end) + end) + validate("buffers", cfg.buffers, function(buffers) + validate("bind_to_cwd", buffers.bind_to_cwd, "boolean") + validate( + "follow_current_file", + buffers.follow_current_file, + schema.Filesystem.FollowCurrentFile + ) + validate("group_empty_dirs", buffers.group_empty_dirs, "boolean") + validate("show_unloaded", buffers.show_unloaded, "boolean") + validate("terminals_first", buffers.terminals_first, "boolean") + validate("renderers", buffers.renderers, schema.Renderers) + validate("window", buffers.window, schema.Window) + end) + validate("git_status", cfg.git_status, function(git_status) + validate("renderers", git_status.renderers, schema.Renderers) + validate("window", git_status.window, schema.Window) + end) + validate("document_symbols", cfg.document_symbols, function(ds) + validate("follow_cursor", ds.follow_cursor, "boolean") + validate("client_filters", ds.client_filters, { "string", "table" }) -- TODO: More specific validation + validate("custom_kinds", ds.custom_kinds, "table") -- TODO: More specific validation + validate("kinds", ds.kinds, "table") + validate("renderers", ds.renderers, schema.Renderers) + validate("window", ds.window, schema.Window) + end) + end, + false, + nil, + function(err) + errors[#errors + 1] = { err } + end, + true + ) + local _end = vim.uv.hrtime() + + if #errors == 0 then + health.ok("Configuration conforms to the neotree.Config.Base schema") + else + for _, err in ipairs(errors) do + health.error(unpack(err)) + end + end + if verbose then + health.info( + "[verbose] Config schema checking is not comprehensive yet, unchecked keys listed below:" + ) + if missed then + for _, miss in ipairs(missed) do + health.info(miss) + end + end + end +end + +function M.check() + health.start("Dependencies") + check_dependencies() + health.start("Configuration") + local config = require("neo-tree").ensure_config() + M.check_config(config) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/health/typecheck.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/health/typecheck.lua new file mode 100644 index 0000000..3271cc2 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/health/typecheck.lua @@ -0,0 +1,213 @@ +local M = {} + +---Like type() but also supports "callable" like neovim does. +---@see _G.type +---@param obj any +---@param expected neotree.LuaType +function M.match(obj, expected) + if type(obj) == expected then + return true + end + if expected == "callable" and vim.is_callable(obj) then + return true + end + return false +end + +---@alias neotree.LuaType type|"callable" +---@alias neotree.health.ValidatorFunction fun(value: T):boolean?,string? +---@alias neotree.health.Validator elem_or_list|neotree.health.ValidatorFunction + +---@type (fun(err:string))[] +M.errfuncs = {} +---@type string[] +M.namestack = {} + +---@generic T : table +---@param path string +---@param tbl T +---@param accesses string[] +---@param missed_paths table +---@return T mocked_tbl +local function mock_recursive(path, tbl, accesses, missed_paths, track_missed) + local mock_table = {} + + ---@class neotree.health.Mock.Metatable : metatable + ---@field accesses string[] + local mt = { + __original_table = tbl, + accesses = accesses, + } + + ---@return string[] missed_paths + mt.get_missed_paths = function() + ---@type string[] + local missed_list = {} + if track_missed then + for p, _ in pairs(missed_paths) do + table.insert(missed_list, p) + end + end + table.sort(missed_list) + return missed_list + end + + mt.__index = function(_, key) + local path_segment + if type(key) == "number" then + path_segment = ("[%02d]"):format(key) + else + path_segment = tostring(key) + end + + local full_path + if path == "" then + full_path = path_segment + elseif type(key) == "number" then + full_path = path .. path_segment + else + full_path = path .. "." .. path_segment + end + + -- Track accesses and missed accesses + mt.accesses[#mt.accesses + 1] = full_path + if track_missed then + missed_paths[full_path] = nil + end + + local value = mt.__original_table[key] + + if type(value) == "table" then + return mock_recursive(full_path, value, mt.accesses, missed_paths, track_missed) + end + return value + end + + setmetatable(mock_table, mt) + return mock_table +end + +--- Wraps a given table in a special mock table that tracks all accesses +--- (reads) to its fields and sub-fields. Optionally tracks unaccessed fields. +--- +---@generic T : table +---@param name string The base name for the table, this forms the root of the access paths. +---@param tbl T The table to be mocked. +---@param track_missed boolean? Track which fields were NOT accessed. +---@return T mocked +function M.mock(name, tbl, track_missed) + local accesses = {} + local path_set = {} + track_missed = track_missed or false + + if track_missed then + -- Generate another mock table and fully traverse that one first + local root_mock = M.mock(name, tbl, false) + + ---@param current_table table + local function deep_traverse_mock(current_table) + ---@type neotree.health.Mock.Metatable + local mt = getmetatable(current_table) + for k, v in pairs(mt.__original_table) do + if type(v) == "table" then + deep_traverse_mock(current_table[k]) + else + mt.__index(nil, k) + end + end + end + deep_traverse_mock(root_mock) + accesses = getmetatable(root_mock).accesses + for _, path in ipairs(accesses) do + path_set[path] = true + end + end + + -- Start the recursive mocking process, passing all necessary shared tracking data. + return mock_recursive(name, tbl, accesses, path_set, track_missed) +end + +---A comprehensive version of vim.validate that makes it easy to validate nested tables of various types +---@generic T +---@param name string +---@param value T +---@param validator neotree.health.Validator +---@param optional? boolean Whether value can be nil +---@param message? string message when validation fails +---@param on_invalid? fun(err: string, value: T):boolean? What to do when a (nested) validation fails, return true to throw error +---@param track_missed? boolean Whether to return a second table that contains every non-checked field +---@return boolean valid +---@return string[]? missed +function M.validate(name, value, validator, optional, message, on_invalid, track_missed) + local matched, errmsg, errinfo + M.namestack[#M.namestack + 1] = name + if type(validator) == "string" then + matched = M.match(value, validator) + elseif type(validator) == "table" then + for _, v in ipairs(validator) do + matched = M.match(value, v) + if matched then + break + end + end + elseif type(validator) == "function" and value ~= nil then + local ok = false + if on_invalid then + M.errfuncs[#M.errfuncs + 1] = on_invalid + end + if track_missed and type(value) == "table" then + value = M.mock(name, value, true) + end + ok, matched, errinfo = pcall(validator, value) + if on_invalid then + M.errfuncs[#M.errfuncs] = nil + end + if not ok then + errinfo = matched + matched = false + elseif matched == nil then + matched = true + end + end + matched = matched or (optional and value == nil) or false + + if not matched then + ---@type string + local expected + if vim.is_callable(validator) then + expected = "?" + else + ---@cast validator -function + local expected_types = type(validator) == "string" and { validator } or validator + ---@cast expected_types -string + if optional then + expected_types[#expected_types + 1] = "nil" + end + expected = table.concat(expected_types, "|") + end + + errmsg = ("%s: %s, got %s"):format( + table.concat(M.namestack, "."), + message or ("expected " .. expected), + message and value or type(value) + ) + if errinfo then + errmsg = errmsg .. ", Info: " .. errinfo + end + local errfunc = M.errfuncs[#M.errfuncs] + local should_error = not errfunc or errfunc(errmsg) + if should_error then + M.namestack[#M.namestack] = nil + error(errmsg, 2) + end + end + M.namestack[#M.namestack] = nil + + if track_missed then + local missed = getmetatable(value).get_missed_paths() + return matched, missed + end + return matched +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/log.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/log.lua new file mode 100644 index 0000000..ba561f9 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/log.lua @@ -0,0 +1,188 @@ +-- log.lua +-- +-- Inspired by rxi/log.lua +-- Modified by tjdevries and can be found at github.com/tjdevries/vlog.nvim +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. + +-- User configuration section +local default_config = { + -- Name of the plugin. Prepended to log messages + plugin = "neo-tree.nvim", + + -- Should print the output to neovim while running + use_console = true, + + -- Should highlighting be used in console (using echohl) + highlights = true, + + -- Should write to a file + use_file = false, + + -- Any messages above this level will be logged. + level = "info", + + -- Level configuration + modes = { + { name = "trace", hl = "None", level = vim.log.levels.TRACE }, + { name = "debug", hl = "None", level = vim.log.levels.DEBUG }, + { name = "info", hl = "None", level = vim.log.levels.INFO }, + { name = "warn", hl = "WarningMsg", level = vim.log.levels.WARN }, + { name = "error", hl = "ErrorMsg", level = vim.log.levels.ERROR }, + { name = "fatal", hl = "ErrorMsg", level = vim.log.levels.ERROR }, + }, + + -- Can limit the number of decimals displayed for floats + float_precision = 0.01, +} + +-- {{{ NO NEED TO CHANGE +local log = {} + +local unpack = unpack + +local notify = function(message, level_config) + if type(vim.notify) == "table" then + -- probably using nvim-notify + vim.notify(message, level_config.level, { title = "Neo-tree" }) + else + local nameupper = level_config.name:upper() + local console_string = string.format("[Neo-tree %s] %s", nameupper, message) + vim.notify(console_string, level_config.level) + end +end + +log.new = function(config, standalone) + config = vim.tbl_deep_extend("force", default_config, config) + + local outfile = + string.format("%s/%s.log", vim.api.nvim_call_function("stdpath", { "data" }), config.plugin) + + local obj + if standalone then + obj = log + else + obj = {} + end + obj.outfile = outfile + + obj.use_file = function(file, quiet) + if file == false then + if not quiet then + obj.info("[neo-tree] Logging to file disabled") + end + config.use_file = false + else + if type(file) == "string" then + obj.outfile = file + else + obj.outfile = outfile + end + config.use_file = true + if not quiet then + obj.info("[neo-tree] Logging to file: " .. obj.outfile) + end + end + end + + local levels = {} + for i, v in ipairs(config.modes) do + levels[v.name] = i + end + + obj.set_level = function(level) + if levels[level] then + if config.level ~= level then + config.level = level + end + else + notify("Invalid log level: " .. level, config.modes[5]) + end + end + + local round = function(x, increment) + increment = increment or 1 + x = x / increment + return (x > 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)) * increment + end + + local make_string = function(...) + local t = {} + for i = 1, select("#", ...) do + local x = select(i, ...) + + if type(x) == "number" and config.float_precision then + x = tostring(round(x, config.float_precision)) + elseif type(x) == "table" then + x = vim.inspect(x) + if #x > 300 then + x = x:sub(1, 300) .. "..." + end + else + x = tostring(x) + end + + t[#t + 1] = x + end + return table.concat(t, " ") + end + + local log_at_level = function(level, level_config, message_maker, ...) + -- Return early if we're below the config.level + if level < levels[config.level] then + return + end + -- Ignore this if vim is exiting + if vim.v.dying > 0 or vim.v.exiting ~= vim.NIL then + return + end + local nameupper = level_config.name:upper() + + local msg = message_maker(...) + local info = debug.getinfo(2, "Sl") + local lineinfo = info.short_src .. ":" .. info.currentline + + -- Output to log file + if config.use_file then + local str = string.format("[%-6s%s] %s: %s\n", nameupper, os.date(), lineinfo, msg) + local fp = io.open(obj.outfile, "a") + if fp then + fp:write(str) + fp:close() + else + print("[neo-tree] Could not open log file: " .. obj.outfile) + end + end + + -- Output to console + if config.use_console and level > 2 then + vim.schedule(function() + notify(msg, level_config) + end) + end + end + + for i, x in ipairs(config.modes) do + obj[x.name] = function(...) + return log_at_level(i, x, make_string, ...) + end + + obj[("fmt_%s"):format(x.name)] = function() + return log_at_level(i, x, function(...) + local passed = { ... } + local fmt = table.remove(passed, 1) + local inspected = {} + for _, v in ipairs(passed) do + table.insert(inspected, vim.inspect(v)) + end + return string.format(fmt, unpack(inspected)) + end) + end + end +end + +log.new(default_config, true) +-- }}} + +return log diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/setup/deprecations.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/setup/deprecations.lua new file mode 100644 index 0000000..89b4bf7 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/setup/deprecations.lua @@ -0,0 +1,136 @@ +local utils = require("neo-tree.utils") + +local M = {} + +local migrations = {} + +M.show_migrations = function() + if #migrations > 0 then + local content = {} + for _, message in ipairs(migrations) do + vim.list_extend(content, vim.split("\n## " .. message, "\n", { trimempty = false })) + end + local header = "# Neo-tree configuration has been updated. Please review the changes below." + table.insert(content, 1, header) + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, content) + vim.bo[buf].buftype = "nofile" + vim.bo[buf].bufhidden = "wipe" + vim.bo[buf].buflisted = false + vim.bo[buf].swapfile = false + vim.bo[buf].modifiable = false + vim.bo[buf].filetype = "markdown" + vim.api.nvim_buf_set_name(buf, "Neo-tree migrations") + vim.defer_fn(function() + vim.cmd(string.format("%ssplit", #content)) + vim.api.nvim_win_set_buf(0, buf) + end, 100) + end +end + +---@param config neotree.Config.Base +M.migrate = function(config) + migrations = {} + + local moved = function(old, new, converter) + local existing = utils.get_value(config, old) + if type(existing) ~= "nil" then + if type(converter) == "function" then + existing = converter(existing) + end + utils.set_value(config, old, nil) + utils.set_value(config, new, existing) + migrations[#migrations + 1] = + string.format("The `%s` option has been deprecated, please use `%s` instead.", old, new) + end + end + + local moved_inside = function(old, new_inside, converter) + local existing = utils.get_value(config, old) + if type(existing) ~= "nil" and type(existing) ~= "table" then + if type(converter) == "function" then + existing = converter(existing) + end + utils.set_value(config, old, {}) + local new = old .. "." .. new_inside + utils.set_value(config, new, existing) + migrations[#migrations + 1] = + string.format("The `%s` option is replaced with a table, please move to `%s`.", old, new) + end + end + + local removed = function(key, desc) + local value = utils.get_value(config, key) + if type(value) ~= "nil" then + utils.set_value(config, key, nil) + migrations[#migrations + 1] = + string.format("The `%s` option has been removed.\n%s", key, desc or "") + end + end + + local renamed_value = function(key, old_value, new_value) + local value = utils.get_value(config, key) + if value == old_value then + utils.set_value(config, key, new_value) + migrations[#migrations + 1] = + string.format("The `%s=%s` option has been renamed to `%s`.", key, old_value, new_value) + end + end + + local opposite = function(value) + return not value + end + + local tab_to_source_migrator = function(labels) + local converted_sources = {} + for entry, label in pairs(labels) do + table.insert(converted_sources, { source = entry, display_name = label }) + end + return converted_sources + end + + moved("filesystem.filters", "filesystem.filtered_items") + moved("filesystem.filters.show_hidden", "filesystem.filtered_items.hide_dotfiles", opposite) + moved("filesystem.filters.respect_gitignore", "filesystem.filtered_items.hide_gitignored") + moved("open_files_do_not_replace_filetypes", "open_files_do_not_replace_types") + moved("source_selector.tab_labels", "source_selector.sources", tab_to_source_migrator) + removed("filesystem.filters.gitignore_source") + removed("filesystem.filter_items.gitignore_source") + renamed_value("filesystem.hijack_netrw_behavior", "open_split", "open_current") + for _, source in ipairs({ "filesystem", "buffers", "git_status" }) do + renamed_value(source .. "window.position", "split", "current") + end + moved_inside("filesystem.follow_current_file", "enabled") + moved_inside("buffers.follow_current_file", "enabled") + + -- v3.x + removed("close_floats_on_escape_key") + + -- v4.x + removed( + "enable_normal_mode_for_inputs", + [[ +Please use `neo_tree_popup_input_ready` event instead and call `stopinsert` inside the handler. + + +See instructions in `:h neo-tree-events` for more details. + +```lua +event_handlers = { + { + event = "neo_tree_popup_input_ready", + ---@param args { bufnr: integer, winid: integer } + handler = function(args) + vim.cmd("stopinsert") + vim.keymap.set("i", "", vim.cmd.stopinsert, { noremap = true, buffer = args.bufnr }) + end, + } +} +``` +]] + ) + + return migrations +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/setup/init.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/setup/init.lua new file mode 100644 index 0000000..a6b058f --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/setup/init.lua @@ -0,0 +1,715 @@ +local utils = require("neo-tree.utils") +local defaults = require("neo-tree.defaults") +local mapping_helper = require("neo-tree.setup.mapping-helper") +local events = require("neo-tree.events") +local log = require("neo-tree.log") +local file_nesting = require("neo-tree.sources.common.file-nesting") +local highlights = require("neo-tree.ui.highlights") +local manager = require("neo-tree.sources.manager") +local netrw = require("neo-tree.setup.netrw") +local hijack_cursor = require("neo-tree.sources.common.hijack_cursor") + +local M = {} + +---@param source_config { window: {mappings: neotree.Config.Window.Mappings} } +local normalize_mappings = function(source_config) + if source_config == nil then + return + end + local mappings = vim.tbl_get(source_config, { "window", "mappings" }) + if mappings then + local fixed = mapping_helper.normalize_mappings(mappings) + source_config.window.mappings = fixed --[[@as neotree.Config.Window.Mappings]] + end +end + +---@param source_config neotree.Config.Filesystem +local normalize_fuzzy_mappings = function(source_config) + if source_config == nil then + return + end + local mappings = source_config.window and source_config.window.fuzzy_finder_mappings + if mappings then + local fixed = mapping_helper.normalize_mappings(mappings) + source_config.window.fuzzy_finder_mappings = fixed --[[@as neotree.Config.FuzzyFinder.Mappings]] + end +end + +local events_setup = false +local define_events = function() + if events_setup then + return + end + + events.define_event(events.FS_EVENT, { + debounce_frequency = 100, + debounce_strategy = utils.debounce_strategy.CALL_LAST_ONLY, + }) + + local v = vim.version() + local diag_autocmd = "DiagnosticChanged" + if v.major < 1 and v.minor < 6 then + diag_autocmd = "User LspDiagnosticsChanged" + end + events.define_autocmd_event(events.VIM_DIAGNOSTIC_CHANGED, { diag_autocmd }, 500, function(args) + args.diagnostics_lookup = utils.get_diagnostic_counts() + return args + end) + + local update_opened_buffers = function(args) + args.opened_buffers = utils.get_opened_buffers() + return args + end + + events.define_autocmd_event(events.VIM_AFTER_SESSION_LOAD, { "SessionLoadPost" }, 200) + events.define_autocmd_event(events.VIM_BUFFER_ADDED, { "BufAdd" }, 200, update_opened_buffers) + events.define_autocmd_event(events.VIM_BUFFER_CHANGED, { "BufWritePost" }, 200) + events.define_autocmd_event( + events.VIM_BUFFER_DELETED, + { "BufDelete" }, + 200, + update_opened_buffers + ) + events.define_autocmd_event(events.VIM_BUFFER_ENTER, { "BufEnter", "BufWinEnter" }, 0) + events.define_autocmd_event( + events.VIM_BUFFER_MODIFIED_SET, + { "BufModifiedSet" }, + 0, + update_opened_buffers + ) + events.define_autocmd_event(events.VIM_COLORSCHEME, { "ColorScheme" }, 0) + events.define_autocmd_event(events.VIM_CURSOR_MOVED, { "CursorMoved" }, 100) + events.define_autocmd_event(events.VIM_DIR_CHANGED, { "DirChanged" }, 200, nil, true) + events.define_autocmd_event(events.VIM_INSERT_LEAVE, { "InsertLeave" }, 200) + events.define_autocmd_event(events.VIM_LEAVE, { "VimLeavePre" }) + events.define_autocmd_event(events.VIM_RESIZED, { "VimResized" }, 100) + events.define_autocmd_event(events.VIM_TAB_CLOSED, { "TabClosed" }) + events.define_autocmd_event(events.VIM_TERMINAL_ENTER, { "TermEnter" }, 0) + events.define_autocmd_event(events.VIM_TEXT_CHANGED_NORMAL, { "TextChanged" }, 200) + events.define_autocmd_event(events.VIM_WIN_CLOSED, { "WinClosed" }) + events.define_autocmd_event(events.VIM_WIN_ENTER, { "WinEnter" }, 0, nil, true) + + events.define_autocmd_event(events.GIT_EVENT, { "User FugitiveChanged" }, 100) + events.define_event(events.GIT_STATUS_CHANGED, { debounce_frequency = 0 }) + events_setup = true + + events.subscribe({ + event = events.VIM_LEAVE, + handler = function() + events.clear_all_events() + end, + }) + + events.subscribe({ + event = events.VIM_RESIZED, + handler = function() + require("neo-tree.ui.renderer").update_floating_window_layouts() + end, + }) +end + +local prior_window_options = {} + +--- Store the current window options so we can restore them when we close the tree. +--- @param winid number | nil The window id to store the options for, defaults to current window +local store_local_window_settings = function(winid) + winid = winid or vim.api.nvim_get_current_win() + local neo_tree_settings_applied, _ = + pcall(vim.api.nvim_win_get_var, winid, "neo_tree_settings_applied") + if neo_tree_settings_applied then + -- don't store our own window settings + return + end + prior_window_options[tostring(winid)] = { + cursorline = vim.wo.cursorline, + cursorlineopt = vim.wo.cursorlineopt, + foldcolumn = vim.wo.foldcolumn, + wrap = vim.wo.wrap, + list = vim.wo.list, + spell = vim.wo.spell, + number = vim.wo.number, + relativenumber = vim.wo.relativenumber, + winhighlight = vim.wo.winhighlight, + } +end + +--- Restore the window options for the current window +--- @param winid number | nil The window id to restore the options for, defaults to current window +local restore_local_window_settings = function(winid) + winid = winid or vim.api.nvim_get_current_win() + -- return local window settings to their prior values + local wo = prior_window_options[tostring(winid)] + if wo then + vim.wo.cursorline = wo.cursorline + vim.wo.cursorlineopt = wo.cursorlineopt + vim.wo.foldcolumn = wo.foldcolumn + vim.wo.wrap = wo.wrap + vim.wo.list = wo.list + vim.wo.spell = wo.spell + vim.wo.number = wo.number + vim.wo.relativenumber = wo.relativenumber + vim.wo.winhighlight = wo.winhighlight + log.debug("Window settings restored") + vim.api.nvim_win_set_var(0, "neo_tree_settings_applied", false) + else + log.debug("No window settings to restore") + end +end + +local last_buffer_enter_filetype = nil +M.buffer_enter_event = function() + -- if it is a neo-tree window, just set local options + if vim.bo.filetype == "neo-tree" then + if last_buffer_enter_filetype == "neo-tree" then + -- we've switched to another neo-tree window + events.fire_event(events.NEO_TREE_BUFFER_LEAVE) + else + store_local_window_settings() + end + vim.cmd([[ + setlocal cursorline + setlocal cursorlineopt=line + setlocal nowrap + setlocal nolist nospell nonumber norelativenumber + ]]) + + local winhighlight = + "Normal:NeoTreeNormal,NormalNC:NeoTreeNormalNC,SignColumn:NeoTreeSignColumn,CursorLine:NeoTreeCursorLine,FloatBorder:NeoTreeFloatBorder,StatusLine:NeoTreeStatusLine,StatusLineNC:NeoTreeStatusLineNC,VertSplit:NeoTreeVertSplit,EndOfBuffer:NeoTreeEndOfBuffer" + if vim.version().minor >= 7 then + vim.cmd("setlocal winhighlight=" .. winhighlight .. ",WinSeparator:NeoTreeWinSeparator") + else + vim.cmd("setlocal winhighlight=" .. winhighlight) + end + + events.fire_event(events.NEO_TREE_BUFFER_ENTER) + last_buffer_enter_filetype = vim.bo.filetype + vim.api.nvim_win_set_var(0, "neo_tree_settings_applied", true) + return + end + + if vim.bo.filetype == "neo-tree-popup" then + vim.cmd([[ + setlocal winhighlight=Normal:NeoTreeFloatNormal,FloatBorder:NeoTreeFloatBorder + setlocal nolist nospell nonumber norelativenumber + ]]) + events.fire_event(events.NEO_TREE_POPUP_BUFFER_ENTER) + last_buffer_enter_filetype = vim.bo.filetype + return + end + + if last_buffer_enter_filetype == "neo-tree" then + events.fire_event(events.NEO_TREE_BUFFER_LEAVE) + end + if last_buffer_enter_filetype == "neo-tree-popup" then + events.fire_event(events.NEO_TREE_POPUP_BUFFER_LEAVE) + end + last_buffer_enter_filetype = vim.bo.filetype + + -- if vim is trying to open a dir, then we hijack it + if netrw.hijack() then + return + end + + -- For all others, make sure another buffer is not hijacking our window + -- ..but not if the position is "current" + local prior_buf = vim.fn.bufnr("#") + if prior_buf < 1 then + return + end + local prior_type = vim.bo[prior_buf].filetype + + -- there is nothing more we want to do with floating windows + -- but when prior_type is neo-tree we might need to redirect buffer somewhere else. + if utils.is_floating() and prior_type ~= "neo-tree" then + return + end + + if prior_type == "neo-tree" then + local success, position = pcall(vim.api.nvim_buf_get_var, prior_buf, "neo_tree_position") + if not success then + -- just bail out now, the rest of these lookups will probably fail too. + return + end + + if position == "current" then + -- nothing to do here, files are supposed to open in same window + return + end + + local current_tabid = vim.api.nvim_get_current_tabpage() + local neo_tree_tabid = vim.api.nvim_buf_get_var(prior_buf, "neo_tree_tabid") + if neo_tree_tabid ~= current_tabid then + -- This a new tab, so the alternate being neo-tree doesn't matter. + return + end + local neo_tree_winid = vim.api.nvim_buf_get_var(prior_buf, "neo_tree_winid") + local current_winid = vim.api.nvim_get_current_win() + if neo_tree_winid ~= current_winid then + -- This is not the neo-tree window, so the alternate being neo-tree doesn't matter. + return + end + + local bufname = vim.api.nvim_buf_get_name(0) + log.debug("redirecting buffer " .. bufname .. " to new split") + vim.cmd("b#") + local win_width = vim.api.nvim_win_get_width(current_winid) + -- Using schedule at this point fixes problem with syntax + -- highlighting in the buffer. I also prevents errors with diagnostics + -- trying to work with the buffer as it's being closed. + vim.schedule(function() + -- try to delete the buffer, only because if it was new it would take + -- on options from the neo-tree window that are undesirable. + ---@diagnostic disable-next-line: param-type-mismatch + pcall(vim.cmd, "bdelete " .. bufname) + local fake_state = { + window = { + position = position, + width = win_width or M.config.window.width, + }, + } + utils.open_file(fake_state, bufname) + end) + end +end + +M.win_enter_event = function() + local win_id = vim.api.nvim_get_current_win() + if utils.is_floating(win_id) then + return + end + -- if the new win is not a floating window, make sure all neo-tree floats are closed + manager.close_all("float") + + if vim.o.filetype == "neo-tree" then + local _, position = pcall(vim.api.nvim_buf_get_var, 0, "neo_tree_position") + if position == "current" then + -- make sure the buffer wasn't moved to a new window + local neo_tree_winid = vim.api.nvim_buf_get_var(0, "neo_tree_winid") + local current_winid = vim.api.nvim_get_current_win() + local current_bufnr = vim.api.nvim_get_current_buf() + if neo_tree_winid ~= current_winid then + -- At this point we know that either the neo-tree window was split, + -- or the neo-tree buffer is being shown in another window for some other reason. + -- Sometime the split is just the first step in the process of opening somethig else, + -- so instead of fixing this right away, we add a short delay and check back again to see + -- if the buffer is still in this window. + local old_state = manager.get_state("filesystem", nil, neo_tree_winid) + vim.schedule(function() + local bufnr = vim.api.nvim_get_current_buf() + if bufnr ~= current_bufnr then + -- The neo-tree buffer was replaced with something else, so we don't need to do anything. + log.trace("neo-tree buffer replaced with something else - no further action required") + return + end + -- create a new tree for this window + local state = manager.get_state("filesystem", nil, current_winid) --[[@as neotree.sources.filesystem.State]] + state.path = old_state.path + state.current_position = "current" + local renderer = require("neo-tree.ui.renderer") + state.force_open_folders = renderer.get_expanded_nodes(old_state.tree) + require("neo-tree.sources.filesystem")._navigate_internal(state, nil, nil, nil, false) + end) + return + end + end + -- it's a neo-tree window, ignore + return + end +end + +M.set_log_level = function(level) + log.set_level(level) +end + +local function merge_global_components_config(components, config) + local indent_exists = false + local merged_components = {} + local do_merge + + do_merge = function(component) + local name = component[1] + if type(name) == "string" then + if name == "indent" then + indent_exists = true + end + local merged = { name } + local global_config = config.default_component_configs[name] + if global_config then + for k, v in pairs(global_config) do + merged[k] = v + end + end + for k, v in pairs(component) do + merged[k] = v + end + if name == "container" then + for i, child in ipairs(component.content) do + merged.content[i] = do_merge(child) + end + end + return merged + else + log.error("component name is the wrong type", component) + end + end + + for _, component in ipairs(components) do + local merged = do_merge(component) + table.insert(merged_components, merged) + end + + -- If the indent component is not specified, then add it. + -- We do this because it used to be implicitly added, so we don't want to + -- break any existing configs. + if not indent_exists then + local indent = { "indent" } + for k, v in pairs(config.default_component_configs.indent or {}) do + indent[k] = v + end + table.insert(merged_components, 1, indent) + end + return merged_components +end + +local merge_renderers = function(default_config, source_default_config, user_config) + -- This can't be a deep copy/merge. If a renderer is specified in the target it completely + -- replaces the base renderer. + + if source_default_config == nil then + -- first override the default config global renderer with the user's global renderers + for name, renderer in pairs(user_config.renderers or {}) do + log.debug("overriding global renderer for " .. name) + default_config.renderers[name] = renderer + end + else + -- then override the global renderers with the source specific renderers + source_default_config.renderers = source_default_config.renderers or {} + for name, renderer in pairs(default_config.renderers or {}) do + if source_default_config.renderers[name] == nil then + log.debug("overriding source renderer for " .. name) + local r = {} + -- Only copy components that exist in the target source. + -- This alllows us to specify global renderers that include components from all sources, + -- even if some of those components are not universal + for _, value in ipairs(renderer) do + if value[1] and source_default_config.components[value[1]] ~= nil then + table.insert(r, value) + end + end + source_default_config.renderers[name] = r + end + end + + -- if user sets renderers, completely wipe the default ones + local source_name = source_default_config.name + for name, _ in pairs(source_default_config.renderers) do + local user = utils.get_value(user_config, source_name .. ".renderers." .. name) + if user then + source_default_config.renderers[name] = nil + end + end + end +end + +---@param user_config neotree.Config? +---@return neotree.Config.Base full_config +M.merge_config = function(user_config) + local default_config = vim.deepcopy(defaults) + user_config = vim.deepcopy(user_config or {}) + + local migrations = require("neo-tree.setup.deprecations").migrate(user_config) + if #migrations > 0 then + -- defer to make sure it is the last message printed + vim.defer_fn(function() + vim.cmd( + "echohl WarningMsg | echo 'Some options have changed, please run `:Neotree migrations` to see the changes' | echohl NONE" + ) + end, 50) + end + + if user_config.log_level ~= nil then + M.set_log_level(user_config.log_level) + end + log.use_file(user_config.log_to_file, true) + log.debug("setup") + + if events_setup then + events.clear_all_events() + end + define_events() + + -- Prevent netrw hijacking lazy-loading from conflicting with normal hijacking. + vim.g.neotree_watching_bufenter = 1 + + -- Prevent accidentally opening another file in the neo-tree window. + events.subscribe({ + event = events.VIM_BUFFER_ENTER, + handler = M.buffer_enter_event, + }) + events.subscribe({ + event = events.NEO_TREE_WINDOW_AFTER_OPEN, + handler = function(args) + if not vim.w[args.winid].neo_tree_settings_applied then + -- TODO: should figure out a less disorganized way to set window options + -- BufEnter doesn't trigger while vim is starting up so this will handle it instead. + M.buffer_enter_event() + end + end, + }) + + -- Setup autocmd for neo-tree BufLeave, to restore window settings. + -- This is set to happen just before leaving the window. + -- The patterns used should ensure it only runs in neo-tree windows where position = "current" + local augroup = vim.api.nvim_create_augroup("NeoTree_BufLeave", { clear = true }) + local bufleave = function(data) + -- Vim patterns in autocmds are not quite precise enough + -- so we are doing a second stage filter in lua + local pattern = "neo%-tree [^ ]+ %[1%d%d%d%]" + if string.match(data.file, pattern) then + restore_local_window_settings() + end + end + vim.api.nvim_create_autocmd({ "BufWinLeave" }, { + group = augroup, + pattern = "neo-tree *", + callback = bufleave, + }) + + if user_config.event_handlers ~= nil then + for _, handler in ipairs(user_config.event_handlers) do + events.subscribe(handler) + end + end + + highlights.setup() + + -- used to either limit the sources that or loaded, or add extra external sources + local all_sources = {} + local all_source_names = {} + for _, source in ipairs(user_config.sources or default_config.sources or {}) do + local parts = utils.split(source, ".") + local name = parts[#parts] + local is_internal_ns, is_external_ns = false, false + local module + + if #parts == 1 then + -- might be a module name in the internal namespace + is_internal_ns, module = pcall(require, "neo-tree.sources." .. source) + end + if is_internal_ns then + name = module.name or name + all_sources[name] = "neo-tree.sources." .. name + else + -- fully qualified module name + -- or just a root level module name + is_external_ns, module = pcall(require, source) + if is_external_ns then + name = module.name or name + all_sources[name] = source + else + log.error("Source module not found", source) + name = nil + end + end + if name then + default_config[name] = module.default_config or default_config[name] + table.insert(all_source_names, name) + end + end + log.debug("Sources to load: ", vim.inspect(all_sources)) + require("neo-tree.command.parser").setup(all_source_names) + + normalize_fuzzy_mappings(default_config.filesystem) + normalize_fuzzy_mappings(user_config.filesystem) + if user_config.use_default_mappings == false then + default_config.filesystem.window.fuzzy_finder_mappings = {} + end + -- setup the default values for all sources + normalize_mappings(default_config) + normalize_mappings(user_config) + merge_renderers(default_config, nil, user_config) + + for source_name, mod_root in pairs(all_sources) do + local module = require(mod_root) + default_config[source_name] = default_config[source_name] + or { + renderers = {}, + components = {}, + } + local source_default_config = default_config[source_name] + source_default_config.components = module.components or require(mod_root .. ".components") + source_default_config.commands = module.commands or require(mod_root .. ".commands") + source_default_config.name = source_name + source_default_config.display_name = module.display_name or source_default_config.name + + if user_config.use_default_mappings == false then + default_config.window.mappings = {} + source_default_config.window.mappings = {} + end + -- Make sure all the mappings are normalized so they will merge properly. + normalize_mappings(source_default_config) + normalize_mappings(user_config[source_name]) + -- merge the global config with the source specific config + source_default_config.window = vim.tbl_deep_extend( + "force", + default_config.window or {}, + source_default_config.window or {}, + user_config.window or {} + ) + + merge_renderers(default_config, source_default_config, user_config) + + --validate the window.position + local pos_key = source_name .. ".window.position" + local position = utils.get_value(user_config, pos_key, "left", true) + local valid_positions = { + left = true, + right = true, + top = true, + bottom = true, + float = true, + current = true, + } + if not valid_positions[position] then + log.error("Invalid value for ", pos_key, ": ", position) + user_config[source_name].window.position = "left" + end + end + + -- local orig_sources = user_config.sources and user_config.sources or {} + + -- apply the users config + M.config = vim.tbl_deep_extend("force", default_config, user_config) + + -- RE: 873, fixes issue with invalid source checking by overriding + -- source table with name table + -- Setting new "sources" to be the parsed names of the sources + M.config.sources = all_source_names + + if + (M.config.source_selector.winbar or M.config.source_selector.statusline) + and M.config.source_selector.sources + and not user_config.default_source + then + -- Set the default source to the head of these + -- This resolves some weirdness with the source selector having + -- a different "head" item than our current default. + -- Removing this line makes Neo-tree show the "filesystem" + -- source instead of whatever the first item in the config is. + -- Probably don't remove this unless you have a better fix for that + M.config.default_source = M.config.source_selector.sources[1].source + end + -- Check if the default source is not included in config.sources + -- log a warning and then "pick" the first in the sources list + local match = false + for _, source in ipairs(M.config.sources) do + if source == M.config.default_source then + match = true + break + end + end + if not match and M.config.default_source ~= "last" then + M.config.default_source = M.config.sources[1] + log.warn( + string.format( + "Invalid default source found in configuration. Using first available source: %s", + M.config.default_source + ) + ) + end + + ---@type neotree.Config.HijackNetrwBehavior[] + local disable_netrw_values = { "open_default", "open_current" } + local hijack_behavior = M.config.filesystem.hijack_netrw_behavior + if vim.tbl_contains(disable_netrw_values, hijack_behavior) then + -- Disable netrw autocmds + vim.cmd("silent! autocmd! FileExplorer *") + elseif hijack_behavior ~= "disabled" then + require("neo-tree.log").error( + "Invalid value for filesystem.hijack_netrw_behavior: '" + .. hijack_behavior + .. "', will default to 'disabled'" + ) + M.config.filesystem.hijack_netrw_behavior = "disabled" + end + + if not M.config.enable_git_status then + M.config.git_status_async = false + end + + -- Validate that the source_selector.sources are all available and if any + -- aren't, remove them + local source_selector_sources = {} + for _, ss_source in ipairs(M.config.source_selector.sources or {}) do + if vim.tbl_contains(M.config.sources, ss_source.source) then + table.insert(source_selector_sources, ss_source) + else + log.debug(string.format("Unable to locate Neo-tree extension %s", ss_source.source)) + end + end + M.config.source_selector.sources = source_selector_sources + + file_nesting.setup(M.config.nesting_rules) + + for source_name, mod_root in pairs(all_sources) do + for name, rndr in pairs(M.config[source_name].renderers) do + M.config[source_name].renderers[name] = merge_global_components_config(rndr, M.config) + end + local module = require(mod_root) + if M.config.commands then + M.config[source_name].commands = + vim.tbl_extend("keep", M.config[source_name].commands or {}, M.config.commands) + end + manager.setup(source_name, M.config[source_name] --[[@as table]], M.config, module) + manager.redraw(source_name) + end + + events.subscribe({ + event = events.VIM_COLORSCHEME, + handler = highlights.setup, + id = "neo-tree-highlight", + }) + + events.subscribe({ + event = events.VIM_WIN_ENTER, + handler = M.win_enter_event, + id = "neo-tree-win-enter", + }) + + --Dispose ourselves if the tab closes + events.subscribe({ + event = events.VIM_TAB_CLOSED, + handler = function(args) + local tabnr = tonumber(args.afile) + log.debug("VIM_TAB_CLOSED: disposing state for tabnr", tabnr) + -- Internally we use tabids to track state but is tabnr of a tab that has already been + -- closed so there is no way to get its tabid. Instead dispose all tabs that are no longer valid. + -- Must be scheduled because nvim_tabpage_is_valid does not work inside TabClosed event callback. + vim.schedule_wrap(manager.dispose_invalid_tabs)() + end, + }) + + --Dispose ourselves if the tab closes + events.subscribe({ + event = events.VIM_WIN_CLOSED, + handler = function(args) + local winid = tonumber(args.afile) + if not winid then + return + end + log.debug("VIM_WIN_CLOSED: disposing state for window", winid) + manager.dispose_window(winid) + end, + }) + + local rt = utils.get_value(M.config, "resize_timer_interval", 50, true) + require("neo-tree.ui.renderer").resize_timer_interval = rt + + if M.config.enable_cursor_hijack then + hijack_cursor.setup() + end + + return M.config +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/setup/mapping-helper.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/setup/mapping-helper.lua new file mode 100644 index 0000000..2467aa4 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/setup/mapping-helper.lua @@ -0,0 +1,76 @@ +local utils = require("neo-tree.utils") + +local M = {} + +---@param key string +M.normalize_map_key = function(key) + if key == nil then + return nil + end + if key:match("^<[^>]+>$") then + local parts = utils.split(key, "-") + if #parts == 2 then + local mod = parts[1]:lower() + if mod == " 2 then + alpha = alpha:lower() + end + key = string.format("%s-%s", mod, alpha) + return key + else + key = key:lower() + if key == "" then + return "" + elseif key == "" then + return "" + elseif key == "" then + return "" + end + end + end + return key +end + +---@class neotree.SimpleMappings +---@field [string] string|function? + +---@class neotree.SimpleMappingsByMode +---@field [string] neotree.SimpleMappings? + +---@class neotree.Mappings : neotree.SimpleMappings +---@field [integer] neotree.SimpleMappingsByMode? + +---@param map neotree.Mappings +---@return neotree.Mappings new_map +M.normalize_mappings = function(map) + local new_map = M.normalize_simple_mappings(map) + ---@cast new_map neotree.Mappings + for i, mappings_by_mode in ipairs(map) do + new_map[i] = {} + for mode, simple_mappings in pairs(mappings_by_mode) do + ---@cast simple_mappings neotree.SimpleMappings + new_map[i][mode] = M.normalize_simple_mappings(simple_mappings) + end + end + return new_map +end + +---@param map neotree.SimpleMappings +---@return neotree.SimpleMappings new_map +M.normalize_simple_mappings = function(map) + local new_map = {} + for key, value in pairs(map) do + if type(key) == "string" then + local normalized_key = M.normalize_map_key(key) + if normalized_key ~= nil then + new_map[normalized_key] = value + end + end + end + return new_map +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/setup/netrw.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/setup/netrw.lua new file mode 100644 index 0000000..c54a329 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/setup/netrw.lua @@ -0,0 +1,102 @@ +local uv = vim.uv or vim.loop +local nt = require("neo-tree") +local utils = require("neo-tree.utils") +local M = {} + +local get_position = function(source_name) + local pos = utils.get_value(nt.config, source_name .. ".window.position", "left", true) + return pos +end + +---@return neotree.Config.HijackNetrwBehavior +M.get_hijack_behavior = function() + nt.ensure_config() + return nt.config.filesystem.hijack_netrw_behavior +end + +---@return boolean hijacked Whether the hijack was successful +M.hijack = function() + local hijack_behavior = M.get_hijack_behavior() + if hijack_behavior == "disabled" then + return false + end + + -- ensure this is a directory + local dir_bufnr = vim.api.nvim_get_current_buf() + local path_to_hijack = vim.api.nvim_buf_get_name(dir_bufnr) + local stats = uv.fs_stat(path_to_hijack) + if not stats or stats.type ~= "directory" then + return false + end + + -- record where we are now + local pos = get_position("filesystem") + local should_open_current = hijack_behavior == "open_current" or pos == "current" + local dir_window = vim.api.nvim_get_current_win() + + -- Now actually open the tree, with a very quick debounce because this may be + -- called multiple times in quick succession. + utils.debounce("hijack_netrw_" .. dir_window, function() + local manager = require("neo-tree.sources.manager") + local log = require("neo-tree.log") + -- We will want to replace the "directory" buffer with either the "alternate" + -- buffer or a new blank one. + local replacement_buffer = vim.fn.bufnr("#") + local is_currently_neo_tree = false + if replacement_buffer > 0 then + if vim.bo[replacement_buffer].filetype == "neo-tree" then + -- don't hijack the current window if it's already a Neo-tree sidebar + local position = vim.b[replacement_buffer].neo_tree_position + if position == "current" then + replacement_buffer = -1 + else + is_currently_neo_tree = true + end + end + end + if not should_open_current then + if replacement_buffer == dir_bufnr or replacement_buffer < 1 then + replacement_buffer = vim.api.nvim_create_buf(true, false) + log.trace("Created new buffer for netrw hijack", replacement_buffer) + end + end + if replacement_buffer > 0 then + log.trace("Replacing buffer in netrw hijack", replacement_buffer) + pcall(vim.api.nvim_win_set_buf, dir_window, replacement_buffer) + end + + -- If a window takes focus (e.g. lazy.nvim installing plugins on startup) in the time between the method call and + -- this debounced callback, we should focus that window over neo-tree. + local current_window = vim.api.nvim_get_current_win() + local should_restore_cursor = current_window ~= dir_window + + local cleanup = vim.schedule_wrap(function() + log.trace("Deleting buffer in netrw hijack", dir_bufnr) + pcall(vim.api.nvim_buf_delete, dir_bufnr, { force = true }) + if should_restore_cursor then + vim.api.nvim_set_current_win(current_window) + end + end) + + ---@type neotree.sources.filesystem.State + local state + if should_open_current and not is_currently_neo_tree then + log.debug("hijack_netrw: opening current") + state = manager.get_state("filesystem", nil, dir_window) --[[@as neotree.sources.filesystem.State]] + state.current_position = "current" + elseif is_currently_neo_tree then + log.debug("hijack_netrw: opening in existing Neo-tree") + state = manager.get_state("filesystem") --[[@as neotree.sources.filesystem.State]] + else + log.debug("hijack_netrw: opening default") + manager.close_all_except("filesystem") + state = manager.get_state("filesystem") --[[@as neotree.sources.filesystem.State]] + end + + require("neo-tree.sources.filesystem")._navigate_internal(state, path_to_hijack, nil, cleanup) + end, 10, utils.debounce_strategy.CALL_LAST_ONLY) + + return true +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/buffers/commands.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/buffers/commands.lua new file mode 100644 index 0000000..7f91a41 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/buffers/commands.lua @@ -0,0 +1,100 @@ +--This file should contain all commands meant to be used by mappings. + +local cc = require("neo-tree.sources.common.commands") +local buffers = require("neo-tree.sources.buffers") +local utils = require("neo-tree.utils") +local manager = require("neo-tree.sources.manager") + +---@class neotree.sources.Buffers.Commands : neotree.sources.Common.Commands +local M = {} + +local refresh = utils.wrap(manager.refresh, "buffers") +local redraw = utils.wrap(manager.redraw, "buffers") + +M.add = function(state) + cc.add(state, refresh) +end + +M.add_directory = function(state) + cc.add_directory(state, refresh) +end + +M.buffer_delete = function(state) + local node = state.tree:get_node() + if node then + if node.type == "message" then + return + end + vim.api.nvim_buf_delete(node.extra.bufnr, { force = false, unload = false }) + refresh() + end +end + +---Marks node as copied, so that it can be pasted somewhere else. +M.copy_to_clipboard = function(state) + cc.copy_to_clipboard(state, redraw) +end + +---@type neotree.TreeCommandVisual +M.copy_to_clipboard_visual = function(state, selected_nodes) + cc.copy_to_clipboard_visual(state, selected_nodes, redraw) +end + +---Marks node as cut, so that it can be pasted (moved) somewhere else. +M.cut_to_clipboard = function(state) + cc.cut_to_clipboard(state, redraw) +end + +---@type neotree.TreeCommandVisual +M.cut_to_clipboard_visual = function(state, selected_nodes) + cc.cut_to_clipboard_visual(state, selected_nodes, redraw) +end + +M.copy = function(state) + cc.copy(state, redraw) +end + +M.move = function(state) + cc.move(state, redraw) +end + +M.show_debug_info = cc.show_debug_info + +---Pastes all items from the clipboard to the current directory. +M.paste_from_clipboard = function(state) + cc.paste_from_clipboard(state, refresh) +end + +M.delete = function(state) + cc.delete(state, refresh) +end + +---Navigate up one level. +M.navigate_up = function(state) + local parent_path, _ = utils.split_path(state.path) + buffers.navigate(state, parent_path) +end + +M.refresh = refresh + +M.rename = function(state) + cc.rename(state, refresh) +end + +M.set_root = function(state) + local node = state.tree:get_node() + while node and node.type ~= "directory" do + local parent_id = node:get_parent_id() + node = parent_id and state.tree:get_node(parent_id) or nil + end + + if not node then + return + end + + buffers.navigate(state, node:get_id()) +end + +cc._add_common_commands(M) + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/buffers/components.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/buffers/components.lua new file mode 100644 index 0000000..fba3856 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/buffers/components.lua @@ -0,0 +1,58 @@ +-- This file contains the built-in components. Each componment is a function +-- that takes the following arguments: +-- config: A table containing the configuration provided by the user +-- when declaring this component in their renderer config. +-- node: A NuiNode object for the currently focused node. +-- state: The current state of the source providing the items. +-- +-- The function should return either a table, or a list of tables, each of which +-- contains the following keys: +-- text: The text to display for this item. +-- highlight: The highlight group to apply to this text. + +local highlights = require("neo-tree.ui.highlights") +local common = require("neo-tree.sources.common.components") +local utils = require("neo-tree.utils") + +---@alias neotree.Component.Buffers._Key +---|"name" + +---@class neotree.Component.Buffers +---@field [1] neotree.Component.Buffers._Key|neotree.Component.Common._Key + +---@type table +local M = {} + +---@class (exact) neotree.Component.Buffers.Name : neotree.Component.Common.Name + +---@param config neotree.Component.Buffers.Name +M.name = function(config, node, state) + local highlight = config.highlight or highlights.FILE_NAME_OPENED + local name = node.name + if node.type == "directory" then + if node:get_depth() == 1 then + highlight = highlights.ROOT_NAME + name = "OPEN BUFFERS in " .. name + else + highlight = highlights.DIRECTORY_NAME + end + elseif node.type == "terminal" then + if node:get_depth() == 1 then + highlight = highlights.ROOT_NAME + name = "TERMINALS" + else + highlight = highlights.FILE_NAME + end + elseif config.use_git_status_colors then + local git_status = state.components.git_status({}, node, state) + if git_status and git_status.highlight then + highlight = git_status.highlight + end + end + return { + text = name, + highlight = highlight, + } +end + +return vim.tbl_deep_extend("force", common, M) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/buffers/init.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/buffers/init.lua new file mode 100644 index 0000000..ded87cc --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/buffers/init.lua @@ -0,0 +1,215 @@ +--This file should have all functions that are in the public api and either set +--or read the state of this source. + +local utils = require("neo-tree.utils") +local renderer = require("neo-tree.ui.renderer") +local items = require("neo-tree.sources.buffers.lib.items") +local events = require("neo-tree.events") +local manager = require("neo-tree.sources.manager") +local git = require("neo-tree.git") + +---@class neotree.sources.Buffers : neotree.Source +local M = { + name = "buffers", + display_name = " 󰈚 Buffers ", +} + +local wrap = function(func) + return utils.wrap(func, M.name) +end + +local get_state = function() + return manager.get_state(M.name) +end + +local follow_internal = function() + if vim.bo.filetype == "neo-tree" or vim.bo.filetype == "neo-tree-popup" then + return + end + local bufnr = vim.api.nvim_get_current_buf() + local path_to_reveal = manager.get_path_to_reveal(true) or tostring(bufnr) + + local state = get_state() + if state.current_position == "float" then + return false + end + if not state.path then + return false + end + local window_exists = renderer.window_exists(state) + if window_exists then + local node = state.tree and state.tree:get_node() + if node then + if node:get_id() == path_to_reveal then + -- already focused + return false + end + end + renderer.focus_node(state, path_to_reveal, true) + end +end + +M.follow = function() + if vim.fn.bufname(0) == "COMMIT_EDITMSG" then + return false + end + utils.debounce("neo-tree-buffer-follow", function() + return follow_internal() + end, 100, utils.debounce_strategy.CALL_LAST_ONLY) +end + +local buffers_changed_internal = function() + for _, tabid in ipairs(vim.api.nvim_list_tabpages()) do + local state = manager.get_state(M.name, tabid) + if state.path and renderer.window_exists(state) then + items.get_opened_buffers(state) + if state.follow_current_file.enabled then + follow_internal() + end + end + end +end + +---Calld by autocmd when any buffer is open, closed, renamed, etc. +M.buffers_changed = function() + utils.debounce( + "buffers_changed", + buffers_changed_internal, + 100, + utils.debounce_strategy.CALL_LAST_ONLY + ) +end + +---Navigate to the given path. +---@param state neotree.State +---@param path string? Path to navigate to. If empty, will navigate to the cwd. +---@param path_to_reveal string? +---@param callback function? +---@param async boolean? +M.navigate = function(state, path, path_to_reveal, callback, async) + state.dirty = false + local path_changed = false + if path == nil then + path = vim.fn.getcwd() + end + if path ~= state.path then + state.path = path + path_changed = true + end + if path_to_reveal then + renderer.position.set(state, path_to_reveal) + end + + items.get_opened_buffers(state) + + if path_changed and state.bind_to_cwd then + vim.api.nvim_command("tcd " .. path) + end + + if type(callback) == "function" then + vim.schedule(callback) + end +end + +---@class neotree.Config.Buffers.Renderers : neotree.Config.Renderers + +---@class (exact) neotree.Config.Buffers : neotree.Config.Source +---@field bind_to_cwd boolean? +---@field follow_current_file neotree.Config.Filesystem.FollowCurrentFile? +---@field group_empty_dirs boolean? +---@field show_unloaded boolean? +---@field terminals_first boolean? +---@field renderers neotree.Config.Buffers.Renderers? + +---Configures the plugin, should be called before the plugin is used. +---@param config neotree.Config.Buffers Configuration table containing any keys that the user wants to change from the defaults. May be empty to accept default values. +---@param global_config neotree.Config.Base +M.setup = function(config, global_config) + --Configure events for before_render + if config.before_render then + --convert to new event system + manager.subscribe(M.name, { + event = events.BEFORE_RENDER, + handler = function(state) + local this_state = get_state() + if state == this_state then + config.before_render(this_state) + end + end, + }) + elseif global_config.enable_git_status then + manager.subscribe(M.name, { + event = events.BEFORE_RENDER, + handler = function(state) + local this_state = get_state() + if state == this_state then + state.git_status_lookup = git.status(state.git_base) + end + end, + }) + manager.subscribe(M.name, { + event = events.GIT_EVENT, + handler = M.buffers_changed, + }) + end + + local refresh_events = { + events.VIM_BUFFER_ADDED, + events.VIM_BUFFER_DELETED, + } + if global_config.enable_refresh_on_write then + table.insert(refresh_events, events.VIM_BUFFER_CHANGED) + end + for _, e in ipairs(refresh_events) do + manager.subscribe(M.name, { + event = e, + handler = function(args) + if args.afile == "" or utils.is_real_file(args.afile) then + M.buffers_changed() + end + end, + }) + end + + if config.bind_to_cwd then + manager.subscribe(M.name, { + event = events.VIM_DIR_CHANGED, + handler = wrap(manager.dir_changed), + }) + end + + if global_config.enable_diagnostics then + manager.subscribe(M.name, { + event = events.STATE_CREATED, + handler = function(state) + state.diagnostics_lookup = utils.get_diagnostic_counts() + end, + }) + manager.subscribe(M.name, { + event = events.VIM_DIAGNOSTIC_CHANGED, + handler = wrap(manager.diagnostics_changed), + }) + end + + --Configure event handlers for modified files + if global_config.enable_modified_markers then + manager.subscribe(M.name, { + event = events.VIM_BUFFER_MODIFIED_SET, + handler = wrap(manager.opened_buffers_changed), + }) + end + + -- Configure event handler for follow_current_file option + if config.follow_current_file.enabled then + manager.subscribe(M.name, { + event = events.VIM_BUFFER_ENTER, + handler = M.follow, + }) + manager.subscribe(M.name, { + event = events.VIM_TERMINAL_ENTER, + handler = M.follow, + }) + end +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/buffers/lib/items.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/buffers/lib/items.lua new file mode 100644 index 0000000..c196117 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/buffers/lib/items.lua @@ -0,0 +1,110 @@ +local renderer = require("neo-tree.ui.renderer") +local utils = require("neo-tree.utils") +local file_items = require("neo-tree.sources.common.file-items") +local log = require("neo-tree.log") + +local M = {} + +---Get a table of all open buffers, along with all parent paths of those buffers. +---The paths are the keys of the table, and all the values are 'true'. +M.get_opened_buffers = function(state) + if state.loading then + return + end + state.loading = true + local context = file_items.create_context() + context.state = state + -- Create root folder + local root = file_items.create_item(context, state.path, "directory") --[[@as neotree.FileItem.Directory]] + root.name = vim.fn.fnamemodify(root.path, ":~") + root.loaded = true + root.search_pattern = state.search_pattern + context.folders[root.path] = root + local terminals = {} + + local function add_buffer(bufnr, path) + local is_loaded = vim.api.nvim_buf_is_loaded(bufnr) + if is_loaded or state.show_unloaded then + local is_listed = vim.fn.buflisted(bufnr) + if is_listed == 1 then + if path == "" then + path = "[No Name]" + end + local success, item = pcall(file_items.create_item, context, path, "file", bufnr) + if success then + item.extra = { + bufnr = bufnr, + is_listed = is_listed, + } + else + log.error("Error creating item for " .. path .. ": " .. item) + end + end + end + end + + local bufs = vim.api.nvim_list_bufs() + for _, b in ipairs(bufs) do + local path = vim.api.nvim_buf_get_name(b) + if vim.startswith(path, "term://") then + local name = path:match("term://(.*)//.*") + local abs_path = vim.fn.fnamemodify(name, ":p") + local has_title, title = pcall(vim.api.nvim_buf_get_var, b, "term_title") + local item = { + name = has_title and title or name, + ext = "terminal", + path = abs_path, + id = path, + type = "terminal", + loaded = true, + extra = { + bufnr = b, + is_listed = true, + }, + } + if utils.is_subpath(state.path, abs_path) then + table.insert(terminals, item) + end + elseif path == "" then + add_buffer(b, path) + else + if #state.path > 1 then + -- make sure this is within the root path + if utils.is_subpath(state.path, path) then + add_buffer(b, path) + end + else + add_buffer(b, path) + end + end + end + + local root_folders = { root } + + if #terminals > 0 then + local terminal_root = { + name = "Terminals", + id = "Terminals", + ext = "terminal", + type = "terminal", + children = terminals, + loaded = true, + search_pattern = state.search_pattern, + } + context.folders["Terminals"] = terminal_root + if state.terminals_first then + table.insert(root_folders, 1, terminal_root) + else + table.insert(root_folders, terminal_root) + end + end + state.default_expanded_nodes = {} + for id, _ in pairs(context.folders) do + table.insert(state.default_expanded_nodes, id) + end + file_items.advanced_sort(root.children, state) + renderer.show_nodes(root_folders, state) + state.loading = false +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/commands.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/commands.lua new file mode 100644 index 0000000..32791b9 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/commands.lua @@ -0,0 +1,965 @@ +--This file should contain all commands meant to be used by mappings. +local fs_actions = require("neo-tree.sources.filesystem.lib.fs_actions") +local utils = require("neo-tree.utils") +local renderer = require("neo-tree.ui.renderer") +local events = require("neo-tree.events") +local inputs = require("neo-tree.ui.inputs") +local popups = require("neo-tree.ui.popups") +local log = require("neo-tree.log") +local help = require("neo-tree.sources.common.help") +local Preview = require("neo-tree.sources.common.preview") +local async = require("plenary.async") +local node_expander = require("neo-tree.sources.common.node_expander") + +---@alias neotree.TreeCommandNormal fun(state: neotree.StateWithTree, ...: any) +---@alias neotree.TreeCommandVisual fun(state: neotree.StateWithTree, selected_nodes: NuiTree.Node[], ...: any) +---@alias neotree.TreeCommand neotree.TreeCommandNormal|neotree.TreeCommandVisual + +---Gets the node parent folder +---@param state neotree.StateWithTree +---@return NuiTree.Node? node +local function get_folder_node(state) + local tree = state.tree + local node = tree:get_node() + local last_id = assert(node):get_id() + + while node do + local insert_as_local = state.config.insert_as + local insert_as_global = require("neo-tree").config.window.insert_as + local use_parent + if insert_as_local then + use_parent = insert_as_local == "sibling" + else + use_parent = insert_as_global == "sibling" + end + + local is_open_dir = node.type == "directory" and (node:is_expanded() or node.empty_expanded) + if use_parent and not is_open_dir then + return tree:get_node(node:get_parent_id()) + end + + if node.type == "directory" then + return node + end + + local parent_id = node:get_parent_id() + if not parent_id or parent_id == last_id then + return node + else + last_id = parent_id + node = tree:get_node(parent_id) + end + end +end + +---The using_root_directory is used to decide what part of the filename to show +-- the user when asking for a new filename to e.g. create, copy to or move to. +---@param state neotree.StateWithTree +---@return string root_path The root path from which the relative source path should be taken +local function get_using_root_directory(state) + -- default to showing only the basename of the path + local using_root_directory = get_folder_node(state):get_id() + local show_path = state.config.show_path + if show_path == "absolute" then + using_root_directory = "" + elseif show_path == "relative" then + using_root_directory = state.path + elseif show_path ~= nil and show_path ~= "none" then + log.warn( + 'A neo-tree mapping was setup with a config.show_path option with invalid value: "' + .. show_path + .. '", falling back to its default: nil/"none"' + ) + end + ---TODO + ---@diagnostic disable-next-line: return-type-mismatch + return using_root_directory +end + +---@class neotree.sources.Common.Commands +---@field [string] neotree.TreeCommand +local M = {} + +---Adds all missing common commands to the given module +---@param to_source_command_module table The commands module for a source +---@param pattern string? A pattern specifying which commands to add, nil to add all +M._add_common_commands = function(to_source_command_module, pattern) + for name, func in pairs(M) do + if + type(name) == "string" + and not to_source_command_module[name] + and (not pattern or name:find(pattern)) + and not name:find("^_") + then + to_source_command_module[name] = func + end + end +end + +---Add a new file or dir at the current node +---@param callback function The callback to call when the command is done. Called with the parent node as the argument. +M.add = function(state, callback) + local node = get_folder_node(state) + if not node then + return + end + local in_directory = node:get_id() + local using_root_directory = get_using_root_directory(state) + fs_actions.create_node(in_directory, callback, using_root_directory) +end + +---Add a new file or dir at the current node +---@param callback function The callback to call when the command is done. Called with the parent node as the argument. +M.add_directory = function(state, callback) + local node = get_folder_node(state) + if not node then + return + end + local in_directory = node:get_id() + local using_root_directory = get_using_root_directory(state) + fs_actions.create_directory(in_directory, callback, using_root_directory) +end + +---Expand all nodes +---@param node table? A single node to expand (defaults to all root nodes) +---@param prefetcher table? an object with two methods `prefetch(state, node)` and `should_prefetch(node) => boolean` +M.expand_all_nodes = function(state, node, prefetcher) + local root_nodes = node and { node } or state.tree:get_nodes() + + renderer.position.set(state, nil) + + local task = function() + for _, root in pairs(root_nodes) do + log.debug("Expanding all nodes under " .. root:get_id()) + node_expander.expand_directory_recursively(state, root, prefetcher) + end + end + async.run(task, function() + log.debug("All nodes expanded - redrawing") + renderer.redraw(state) + end) +end + +---Expand all subnodes +---@param node table? A single node to expand (defaults to node under the cursor) +---@param prefetcher table? an object with two methods `prefetch(state, node)` and `should_prefetch(node) => boolean` +M.expand_all_subnodes = function(state, node, prefetcher) + M.expand_all_nodes(state, node or state.tree:get_node(), prefetcher) +end + +---@param callback function +M.close_node = function(state, callback) + local tree = state.tree + local node = assert(tree:get_node()) + local parent_node = tree:get_node(node:get_parent_id()) + local target_node + + if node:has_children() and node:is_expanded() then + target_node = node + else + target_node = parent_node + end + assert(target_node, "no node found to close") + + local root = tree:get_nodes()[1] + local is_root = target_node:get_id() == root:get_id() + + if target_node:has_children() and not is_root then + target_node:collapse() + renderer.redraw(state) + renderer.focus_node(state, target_node:get_id()) + if state.explicitly_opened_nodes and state.explicitly_opened_nodes[target_node:get_id()] then + state.explicitly_opened_nodes[target_node:get_id()] = false + end + end +end + +M.close_all_subnodes = function(state) + local tree = state.tree + local node = assert(tree:get_node()) + local parent_node = assert(tree:get_node(node:get_parent_id())) + local target_node + + if node:has_children() and node:is_expanded() then + target_node = node + else + target_node = parent_node + end + + renderer.collapse_all_nodes(tree, target_node:get_id()) + renderer.redraw(state) + renderer.focus_node(state, target_node:get_id()) + if state.explicitly_opened_nodes and state.explicitly_opened_nodes[target_node:get_id()] then + state.explicitly_opened_nodes[target_node:get_id()] = false + end +end + +---@param state neotree.State +M.close_all_nodes = function(state) + state.explicitly_opened_nodes = {} + renderer.collapse_all_nodes(state.tree) + renderer.redraw(state) +end + +---@param state neotree.State +M.close_window = function(state) + renderer.close(state) +end + +---@param state neotree.State +M.toggle_auto_expand_width = function(state) + if state.window.position == "float" then + return + end + state.window.auto_expand_width = state.window.auto_expand_width == false + local width = utils.resolve_width(state.window.width) + if not state.window.auto_expand_width then + if (state.window.last_user_width or width) >= vim.api.nvim_win_get_width(0) then + state.window.last_user_width = width + end + vim.api.nvim_win_set_width(0, state.window.last_user_width) + state.win_width = state.window.last_user_width + state.longest_width_exact = 0 + log.trace(string.format("Collapse auto_expand_width.")) + end + renderer.redraw(state) +end + +---@param state neotree.State +local copy_node_to_clipboard = function(state, node) + state.clipboard = state.clipboard or {} + local existing = state.clipboard[node.id] + if existing and existing.action == "copy" then + state.clipboard[node.id] = nil + else + state.clipboard[node.id] = { action = "copy", node = node } + log.info("Copied " .. node.name .. " to clipboard") + end +end + +---Marks node as copied, so that it can be pasted somewhere else. +---@param state neotree.State +M.copy_to_clipboard = function(state, callback) + local node = assert(state.tree:get_node()) + if node.type == "message" then + return + end + copy_node_to_clipboard(state, node) + if callback then + callback() + end +end + +---@type neotree.TreeCommandVisual +M.copy_to_clipboard_visual = function(state, selected_nodes, callback) + for _, node in ipairs(selected_nodes) do + if node.type ~= "message" then + copy_node_to_clipboard(state, node) + end + end + if callback then + callback() + end +end + +---@param state neotree.State +---@param node NuiTree.Node +local cut_node_to_clipboard = function(state, node) + state.clipboard = state.clipboard or {} + local existing = state.clipboard[node.id] + if existing and existing.action == "cut" then + state.clipboard[node.id] = nil + else + state.clipboard[node.id] = { action = "cut", node = node } + log.info("Cut " .. node.name .. " to clipboard") + end +end + +---Marks node as cut, so that it can be pasted (moved) somewhere else. +M.cut_to_clipboard = function(state, callback) + local node = assert(state.tree:get_node()) + cut_node_to_clipboard(state, node) + if callback then + callback() + end +end + +---@type neotree.TreeCommandVisual +M.cut_to_clipboard_visual = function(state, selected_nodes, callback) + for _, node in ipairs(selected_nodes) do + if node.type ~= "message" then + cut_node_to_clipboard(state, node) + end + end + if callback then + callback() + end +end + +-------------------------------------------------------------------------------- +-- Git commands +-------------------------------------------------------------------------------- + +---@param state neotree.State +M.git_add_file = function(state) + local node = assert(state.tree:get_node()) + if node.type == "message" then + return + end + local path = node:get_id() + local cmd = { "git", "add", path } + vim.fn.system(cmd) + events.fire_event(events.GIT_EVENT) +end + +---@param state neotree.State +M.git_add_all = function(state) + local cmd = { "git", "add", "-A" } + vim.fn.system(cmd) + events.fire_event(events.GIT_EVENT) +end + +---@param state neotree.State +M.git_commit = function(state, and_push) + local width = vim.fn.winwidth(0) - 2 + local row = vim.api.nvim_win_get_height(0) - 3 + local popup_options = { + relative = "win", + position = { + row = row, + col = 0, + }, + size = width, + } + + inputs.input("Commit message: ", "", function(msg) + local cmd = { "git", "commit", "-m", msg } + local title = "git commit" + local result = vim.fn.systemlist(cmd) + if vim.v.shell_error ~= 0 or (#result > 0 and vim.startswith(result[1], "fatal:")) then + popups.alert("ERROR: git commit", result) + return + end + if and_push then + title = "git commit && git push" + cmd = { "git", "push" } + local result2 = vim.fn.systemlist(cmd) + table.insert(result, "") + for i = 1, #result2 do + table.insert(result, result2[i]) + end + end + events.fire_event(events.GIT_EVENT) + popups.alert(title, result) + end, popup_options) +end + +M.git_commit_and_push = function(state) + M.git_commit(state, true) +end + +M.git_push = function(state) + inputs.confirm("Are you sure you want to push your changes?", function(yes) + if yes then + local result = vim.fn.systemlist({ "git", "push" }) + events.fire_event(events.GIT_EVENT) + popups.alert("git push", result) + end + end) +end + +M.git_unstage_file = function(state) + local node = assert(state.tree:get_node()) + if node.type == "message" then + return + end + local path = node:get_id() + local cmd = { "git", "reset", "--", path } + vim.fn.system(cmd) + events.fire_event(events.GIT_EVENT) +end + +M.git_undo_last_commit = function(state) + inputs.confirm("Are you sure you want to undo the last commit? (keeps changes)", function(yes) + if yes then + local cmd = { "git", "reset", "--soft", "HEAD~1" } + local result = vim.fn.systemlist(cmd) + if vim.v.shell_error ~= 0 then + popups.alert("ERROR: git reset --soft HEAD~1", result) + return + end + events.fire_event(events.GIT_EVENT) + popups.alert( + "git reset --soft HEAD~1", + { "Last commit undone successfully", "Changes kept in staging area" } + ) + end + end) +end + +M.git_revert_file = function(state) + local node = assert(state.tree:get_node()) + if node.type == "message" then + return + end + local path = node:get_id() + local cmd = { "git", "checkout", "HEAD", "--", path } + local msg = string.format("Are you sure you want to revert %s?", node.name) + inputs.confirm(msg, function(yes) + if yes then + vim.fn.system(cmd) + events.fire_event(events.GIT_EVENT) + end + end) +end + +-------------------------------------------------------------------------------- +-- END Git commands +-------------------------------------------------------------------------------- + +local get_sources = function() + local config = require("neo-tree").config + return config.source_selector.sources or config.sources +end + +M.next_source = function(state) + local sources = get_sources() + local next_source = sources[1] + for i, source_info in ipairs(sources) do + if source_info.source == state.name then + next_source = sources[i + 1] + if not next_source then + next_source = sources[1] + end + break + end + end + + require("neo-tree.command").execute({ + source = next_source.source, + position = state.current_position, + action = "focus", + }) +end + +M.prev_source = function(state) + local sources = get_sources() + local next_source = sources[#sources] + for i, source_info in ipairs(sources) do + if source_info.source == state.name then + next_source = sources[i - 1] + if not next_source then + next_source = sources[#sources] + end + break + end + end + + require("neo-tree.command").execute({ + source = next_source.source, + position = state.current_position, + action = "focus", + }) +end + +local function set_sort(state, label) + local sort = state.sort or { label = "Name", direction = -1 } + if sort.label == label then + sort.direction = sort.direction * -1 + else + sort.label = label + sort.direction = -1 + end + state.sort = sort +end + +M.order_by_created = function(state) + set_sort(state, "Created") + state.sort_field_provider = function(node) + local stat = utils.get_stat(node) + return stat.birthtime and stat.birthtime.sec or 0 + end + require("neo-tree.sources.manager").refresh(state.name) +end + +M.order_by_modified = function(state) + set_sort(state, "Last Modified") + state.sort_field_provider = function(node) + local stat = utils.get_stat(node) + return stat.mtime and stat.mtime.sec or 0 + end + require("neo-tree.sources.manager").refresh(state.name) +end + +M.order_by_name = function(state) + set_sort(state, "Name") + local config = require("neo-tree").config + if config.sort_case_insensitive then + state.sort_field_provider = function(node) + return node.path:lower() + end + else + state.sort_field_provider = function(node) + return node.path + end + end + require("neo-tree.sources.manager").refresh(state.name) +end + +M.order_by_size = function(state) + set_sort(state, "Size") + state.sort_field_provider = function(node) + local stat = utils.get_stat(node) + return stat.size or 0 + end + require("neo-tree.sources.manager").refresh(state.name) +end + +M.order_by_type = function(state) + set_sort(state, "Type") + state.sort_field_provider = function(node) + return node.ext or node.type + end + require("neo-tree.sources.manager").refresh(state.name) +end + +M.order_by_git_status = function(state) + set_sort(state, "Git Status") + + state.sort_field_provider = function(node) + local git_status_lookup = state.git_status_lookup or {} + local git_status = git_status_lookup[node.path] + if git_status then + return git_status + end + + if node.filtered_by and node.filtered_by.gitignored then + return "!!" + else + return "" + end + end + + require("neo-tree.sources.manager").refresh(state.name) +end + +M.order_by_diagnostics = function(state) + set_sort(state, "Diagnostics") + + state.sort_field_provider = function(node) + local diag = state.diagnostics_lookup or {} + local diagnostics = diag[node.path] + if not diagnostics then + return 0 + end + if not diagnostics.severity_number then + return 0 + end + -- lower severity number means higher severity + return 5 - diagnostics.severity_number + end + + require("neo-tree.sources.manager").refresh(state.name) +end + +M.show_debug_info = function(state) + print(vim.inspect(state)) +end + +local default_filetime_format = "%Y-%m-%d %I:%M %p" +M.show_file_details = function(state) + local node = assert(state.tree:get_node()) + if node.type == "message" then + return + end + local stat = utils.get_stat(node) + local left = {} + local right = {} + table.insert(left, "Name") + table.insert(right, node.name) + table.insert(left, "Path") + table.insert(right, node:get_id()) + table.insert(left, "Type") + table.insert(right, node.type) + if stat.size then + table.insert(left, "Size") + table.insert(right, utils.human_size(stat.size)) + table.insert(left, "Created") + local created_format = state.config.created_format or default_filetime_format + table.insert(right, utils.date(created_format, stat.birthtime.sec)) + table.insert(left, "Modified") + local modified_format = state.config.modified_format or default_filetime_format + table.insert(right, utils.date(modified_format, stat.mtime.sec)) + end + + local lines = {} + for i, v in ipairs(left) do + local line = string.format("%9s: %s", v, right[i]) + table.insert(lines, line) + end + + popups.alert("File Details", lines) +end + +---Pastes all items from the clipboard to the current directory. +---@param callback fun(node: NuiTree.Node?, destination: string) The callback to call when the command is done. Called with the parent node as the argument. +M.paste_from_clipboard = function(state, callback) + if state.clipboard then + local folder = get_folder_node(state):get_id() + -- Convert to list so to make it easier to pop items from the stack. + local clipboard_list = {} + for _, item in pairs(state.clipboard) do + table.insert(clipboard_list, item) + end + state.clipboard = nil + local handle_next_paste, paste_complete + + paste_complete = function(source, destination) + if callback then + local insert_as = require("neo-tree").config.window.insert_as + -- open the folder so the user can see the new files + local node = insert_as == "sibling" and state.tree:get_node() or state.tree:get_node(folder) + if not node then + log.warn("Could not find node for " .. folder) + end + callback(node, destination) + end + local next_item = table.remove(clipboard_list) + if next_item then + handle_next_paste(next_item) + end + end + + handle_next_paste = function(item) + if item.action == "copy" then + fs_actions.copy_node( + item.node.path, + folder .. utils.path_separator .. item.node.name, + paste_complete + ) + elseif item.action == "cut" then + fs_actions.move_node( + item.node.path, + folder .. utils.path_separator .. item.node.name, + paste_complete + ) + end + end + + local next_item = table.remove(clipboard_list) + if next_item then + handle_next_paste(next_item) + end + end +end + +---Copies a node to a new location, using typed input. +---@param callback fun(parent_node: NuiTree.Node) +M.copy = function(state, callback) + local node = assert(state.tree:get_node()) + if node.type == "message" then + return + end + local using_root_directory = get_using_root_directory(state) + fs_actions.copy_node(node.path, nil, callback, using_root_directory) +end + +---Moves a node to a new location, using typed input. +---@param callback fun(parent_node: NuiTree.Node) +M.move = function(state, callback) + local node = assert(state.tree:get_node()) + if node.type == "message" then + return + end + local using_root_directory = get_using_root_directory(state) + fs_actions.move_node(node.path, nil, callback, using_root_directory) +end + +M.delete = function(state, callback) + local node = assert(state.tree:get_node()) + if node.type ~= "file" and node.type ~= "directory" then + log.warn("The `delete` command can only be used on files and directories") + return + end + if node:get_depth() == 1 then + log.error( + "Will not delete root node " + .. node.path + .. ", please back out of the current directory if you want to delete the root node." + ) + return + end + fs_actions.delete_node(node.path, callback) +end + +---@param callback function +---@type neotree.TreeCommandVisual +M.delete_visual = function(state, selected_nodes, callback) + local paths_to_delete = {} + for _, node_to_delete in pairs(selected_nodes) do + if node_to_delete:get_depth() == 1 then + log.error( + "Will not delete root node " + .. node_to_delete.path + .. ", please back out of the current directory if you want to delete the root node." + ) + return + end + + if node_to_delete.type == "file" or node_to_delete.type == "directory" then + table.insert(paths_to_delete, node_to_delete.path) + end + end + fs_actions.delete_nodes(paths_to_delete, callback) +end + +M.preview = function(state) + Preview.show(state) +end + +M.revert_preview = function() + Preview.hide() +end +-- +-- Multi-purpose function to back out of whatever we are in +M.cancel = function(state) + if Preview.is_active() then + Preview.hide() + else + if state.current_position == "float" then + renderer.close_all_floating_windows() + end + end +end + +M.toggle_preview = function(state) + Preview.toggle(state) +end + +M.scroll_preview = function(state) + Preview.scroll(state) +end + +M.focus_preview = function(state) + if Preview.is_active() then + Preview.focus() + else + vim.api.nvim_win_call(state.winid, function() + vim.api.nvim_feedkeys(state.fallback, "n", false) + end) + end +end + +---Expands or collapses the current node. +M.toggle_node = function(state, toggle_directory) + local tree = state.tree + local node = assert(tree:get_node()) + if not utils.is_expandable(node) then + return + end + if node.type == "directory" and toggle_directory then + toggle_directory(node) + elseif node:has_children() then + local updated = false + if node:is_expanded() then + updated = node:collapse() + else + updated = node:expand() + end + if updated then + renderer.redraw(state) + end + end +end + +---Expands or collapses the current node. +M.toggle_directory = function(state, toggle_directory) + local tree = state.tree + local node = assert(tree:get_node()) + if node.type ~= "directory" then + return + end + M.toggle_node(state, toggle_directory) +end + +---Open file or expandable node +---@param open_cmd string The vim command to use to open the file +---@param toggle_directory function The function to call to toggle a directory +---open/closed +local open_with_cmd = function(state, open_cmd, toggle_directory, open_file) + local tree = state.tree + local success, node = pcall(tree.get_node, tree) + if not (success and node) then + log.debug("Could not get node.") + return + end + + local function open() + M.revert_preview() + local path = node.path or node:get_id() + local bufnr = node.extra and node.extra.bufnr + if node.type == "terminal" then + path = node:get_id() + end + if type(open_file) == "function" then + open_file(state, path, open_cmd, bufnr) + else + utils.open_file(state, path, open_cmd, bufnr) + end + local extra = node.extra or {} + local pos = extra.position or extra.end_position + if pos ~= nil then + vim.api.nvim_win_set_cursor(0, { (pos[1] or 0) + 1, pos[2] or 0 }) + vim.api.nvim_win_call(0, function() + vim.cmd("normal! zvzz") -- expand folds and center cursor + end) + end + end + + local config = state.config or {} + if node.type == "file" and config.no_expand_file ~= nil then + log.warn("`no_expand_file` options is deprecated, move to `expand_nested_files` (OPPOSITE)") + config.expand_nested_files = not config.no_expand_file + end + + local should_expand_file = config.expand_nested_files and not node:is_expanded() + if utils.is_expandable(node) and (node.type ~= "file" or should_expand_file) then + M.toggle_node(state, toggle_directory) + else + open() + end +end + +---Open file or directory in the closest window +---@param toggle_directory function The function to call to toggle a directory +---open/closed +M.open = function(state, toggle_directory) + open_with_cmd(state, "e", toggle_directory) +end + +---Open file or directory in a split of the closest window +---@param toggle_directory function The function to call to toggle a directory +---open/closed +M.open_split = function(state, toggle_directory) + open_with_cmd(state, "split", toggle_directory) +end + +---Open file or directory in a vertical split of the closest window +---@param toggle_directory function The function to call to toggle a directory +---open/closed +M.open_vsplit = function(state, toggle_directory) + open_with_cmd(state, "vsplit", toggle_directory) +end + +---Open file or directory in a right below vertical split of the closest window +---@param toggle_directory function The function to call to toggle a directory +---open/closed +M.open_rightbelow_vs = function(state, toggle_directory) + open_with_cmd(state, "rightbelow vs", toggle_directory) +end + +---Open file or directory in a left above vertical split of the closest window +---@param toggle_directory function The function to call to toggle a directory +---open/closed +M.open_leftabove_vs = function(state, toggle_directory) + open_with_cmd(state, "leftabove vs", toggle_directory) +end + +---Open file or directory in a new tab +---@param toggle_directory function The function to call to toggle a directory +---open/closed +M.open_tabnew = function(state, toggle_directory) + open_with_cmd(state, "tabnew", toggle_directory) +end + +---Open file or directory or focus it if a buffer already exists with it +---@param toggle_directory function The function to call to toggle a directory +---open/closed +M.open_drop = function(state, toggle_directory) + open_with_cmd(state, "drop", toggle_directory) +end + +---Open file or directory in new tab or focus it if a buffer already exists with it +---@param toggle_directory function The function to call to toggle a directory +---open/closed +M.open_tab_drop = function(state, toggle_directory) + open_with_cmd(state, "tab drop", toggle_directory) +end + +M.rename = function(state, callback) + local tree = state.tree + local node = assert(tree:get_node()) + if node.type == "message" then + return + end + fs_actions.rename_node(node.path, callback) +end + +M.rename_basename = function(state, callback) + local tree = state.tree + local node = assert(tree:get_node()) + if node.type == "message" then + return + end + fs_actions.rename_node_basename(node.path, callback) +end + +---Marks potential windows with letters and will open the give node in the picked window. +---@param state neotree.State +---@param path string The path to open +---@param cmd string Command that is used to perform action on picked window +local use_window_picker = function(state, path, cmd) + local success, picker = pcall(require, "window-picker") + if not success then + print( + "You'll need to install window-picker to use this command: https://github.com/s1n7ax/nvim-window-picker" + ) + return + end + local events = require("neo-tree.events") + local event_result = events.fire_event(events.FILE_OPEN_REQUESTED, { + state = state, + path = path, + open_cmd = cmd, + }) or {} + if event_result.handled then + events.fire_event(events.FILE_OPENED, path) + return + end + local picked_window_id = picker.pick_window() + if picked_window_id then + vim.api.nvim_set_current_win(picked_window_id) + ---@diagnostic disable-next-line: param-type-mismatch + local result, err = pcall(vim.cmd, cmd .. " " .. vim.fn.fnameescape(path)) + if result or err == "Vim(edit):E325: ATTENTION" then + -- fixes #321 + vim.bo[0].buflisted = true + events.fire_event(events.FILE_OPENED, path) + else + log.error("Error opening file:", err) + end + end +end + +---Marks potential windows with letters and will open the give node in the picked window. +M.open_with_window_picker = function(state, toggle_directory) + open_with_cmd(state, "edit", toggle_directory, use_window_picker) +end + +---Marks potential windows with letters and will open the give node in a split next to the picked window. +M.split_with_window_picker = function(state, toggle_directory) + open_with_cmd(state, "split", toggle_directory, use_window_picker) +end + +---Marks potential windows with letters and will open the give node in a vertical split next to the picked window. +M.vsplit_with_window_picker = function(state, toggle_directory) + open_with_cmd(state, "vsplit", toggle_directory, use_window_picker) +end + +M.show_help = function(state) + local title = state.config and state.config.title or nil + local prefix_key = state.config and state.config.prefix_key or nil + help.show(state, title, prefix_key) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/components.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/components.lua new file mode 100644 index 0000000..2865711 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/components.lua @@ -0,0 +1,720 @@ +-- This file contains the built-in components. Each componment is a function +-- that takes the following arguments: +-- config: A table containing the configuration provided by the user +-- when declaring this component in their renderer config. +-- node: A NuiNode object for the currently focused node. +-- state: The current state of the source providing the items. +-- +-- The function should return either a table, or a list of tables, each of which +-- contains the following keys: +-- text: The text to display for this item. +-- highlight: The highlight group to apply to this text. + +local highlights = require("neo-tree.ui.highlights") +local utils = require("neo-tree.utils") +local file_nesting = require("neo-tree.sources.common.file-nesting") +local container = require("neo-tree.sources.common.container") +local nt = require("neo-tree") + +---@alias neotree.Component.Common._Key +---|"bufnr" +---|"clipboard" +---|"container" +---|"current_filter" +---|"diagnostics" +---|"git_status" +---|"filtered_by" +---|"icon" +---|"modified" +---|"name" +---|"indent" +---|"file_size" +---|"last_modified" +---|"created" +---|"symlink_target" +---|"type" + +---@class neotree.Component.Common Use the neotree.Component.Common.* types to get more specific types. +---@field [1] neotree.Component.Common._Key + +---@type table +local M = {} + +local make_two_char = function(symbol) + if vim.fn.strchars(symbol) == 1 then + return symbol .. " " + else + return symbol + end +end + +---@class (exact) neotree.Component.Common.Bufnr : neotree.Component +---@field [1] "bufnr"? + +-- Config fields below: +-- only works in the buffers component, but it's here so we don't have to defined +-- multple renderers. +---@param config neotree.Component.Common.Bufnr +M.bufnr = function(config, node, _) + local highlight = config.highlight or highlights.BUFFER_NUMBER + local bufnr = node.extra and node.extra.bufnr + if not bufnr then + return {} + end + return { + text = string.format("#%s", bufnr), + highlight = highlight, + } +end + +---@class (exact) neotree.Component.Common.Clipboard : neotree.Component +---@field [1] "clipboard"? + +---@param config neotree.Component.Common.Clipboard +M.clipboard = function(config, node, state) + local clipboard = state.clipboard or {} + local clipboard_state = clipboard[node:get_id()] + if not clipboard_state then + return {} + end + return { + text = " (" .. clipboard_state.action .. ")", + highlight = config.highlight or highlights.DIM_TEXT, + } +end + +---@class (exact) neotree.Component.Common.Container : neotree.Component +---@field [1] "container"? +---@field left_padding integer? +---@field right_padding integer? +---@field enable_character_fade boolean? +---@field content (neotree.Component|{zindex: number, align: "left"|"right"|nil})[]? + +M.container = container.render + +---@class (exact) neotree.Component.Common.CurrentFilter : neotree.Component +---@field [1] "current_filter" + +---@param config neotree.Component.Common.CurrentFilter +M.current_filter = function(config, node, _) + local filter = node.search_pattern or "" + if filter == "" then + return {} + end + return { + { + text = "Find", + highlight = highlights.DIM_TEXT, + }, + { + text = string.format('"%s"', filter), + highlight = config.highlight or highlights.FILTER_TERM, + }, + { + text = "in", + highlight = highlights.DIM_TEXT, + }, + } +end + +---`sign_getdefined` based wrapper with compatibility +---@param severity string +---@return vim.fn.sign_getdefined.ret.item +local get_legacy_sign = function(severity) + local sign = vim.fn.sign_getdefined("DiagnosticSign" .. severity) + if vim.tbl_isempty(sign) then + -- backwards compatibility... + local old_severity = severity + if severity == "Warning" then + old_severity = "Warn" + elseif severity == "Information" then + old_severity = "Info" + end + sign = vim.fn.sign_getdefined("LspDiagnosticsSign" .. old_severity) + end + return sign and sign[1] +end + +local nvim_0_10 = vim.fn.has("nvim-0.10") > 0 +---Returns the sign corresponding to the given severity +---@param severity string +---@return vim.fn.sign_getdefined.ret.item +local function get_diagnostic_sign(severity) + local sign + + if nvim_0_10 then + local signs = vim.diagnostic.config().signs + + if type(signs) == "function" then + --TODO: Find a better way to get a namespace + local namespaces = vim.diagnostic.get_namespaces() + if not vim.tbl_isempty(namespaces) then + local ns_id = next(namespaces) + ---@cast ns_id -nil + signs = signs(ns_id, 0) + end + end + + if type(signs) == "table" then + local identifier = severity:sub(1, 1) + if identifier == "H" then + identifier = "N" + end + sign = { + text = (signs.text or {})[vim.diagnostic.severity[identifier]], + texthl = "DiagnosticSign" .. severity, + } + elseif signs == true then + sign = get_legacy_sign(severity) + end + else -- before 0.10 + sign = get_legacy_sign(severity) + end + + if type(sign) ~= "table" then + sign = {} + end + return sign +end + +---@class (exact) neotree.Component.Common.Diagnostics : neotree.Component +---@field [1] "diagnostics"? +---@field errors_only boolean? +---@field hide_when_expanded boolean? +---@field symbols table? +---@field highlights table? + +---@param config neotree.Component.Common.Diagnostics +M.diagnostics = function(config, node, state) + local diag = state.diagnostics_lookup or {} + local diag_state = utils.index_by_path(diag, node:get_id()) + if config.hide_when_expanded and node.type == "directory" and node:is_expanded() then + return {} + end + if not diag_state then + return {} + end + if config.errors_only and diag_state.severity_number > 1 then + return {} + end + ---@type string + local severity = diag_state.severity_string + local sign = get_diagnostic_sign(severity) + + -- check for overrides in the component config + local severity_lower = severity:lower() + if config.symbols and config.symbols[severity_lower] then + sign.texthl = sign.texthl or ("Diagnostic" .. severity) + sign.text = config.symbols[severity_lower] + end + if config.highlights and config.highlights[severity_lower] then + sign.text = sign.text or severity:sub(1, 1) + sign.texthl = config.highlights[severity_lower] + end + + if sign.text and sign.texthl then + return { + text = make_two_char(sign.text), + highlight = sign.texthl, + } + else + return { + text = severity:sub(1, 1), + highlight = "Diagnostic" .. severity, + } + end +end + +---@class (exact) neotree.Component.Common.GitStatus : neotree.Component +---@field [1] "git_status"? +---@field hide_when_expanded boolean? +---@field symbols table? + +---@param config neotree.Component.Common.GitStatus +M.git_status = function(config, node, state) + local git_status_lookup = state.git_status_lookup + if config.hide_when_expanded and node.type == "directory" and node:is_expanded() then + return {} + end + if not git_status_lookup then + return {} + end + local git_status = git_status_lookup[node.path] + if not git_status then + if node.filtered_by and node.filtered_by.gitignored then + git_status = "!!" + else + return {} + end + end + + local symbols = config.symbols or {} + local change_symbol + local change_highlt = highlights.FILE_NAME + ---@type string? + local status_symbol = symbols.staged + local status_highlt = highlights.GIT_STAGED + if node.type == "directory" and git_status:len() == 1 then + status_symbol = nil + end + + if git_status:sub(1, 1) == " " then + status_symbol = symbols.unstaged + status_highlt = highlights.GIT_UNSTAGED + end + + if git_status:match("?$") then + status_symbol = nil + status_highlt = highlights.GIT_UNTRACKED + change_symbol = symbols.untracked + change_highlt = highlights.GIT_UNTRACKED + -- all variations of merge conflicts + elseif git_status == "DD" then + status_symbol = symbols.conflict + status_highlt = highlights.GIT_CONFLICT + change_symbol = symbols.deleted + change_highlt = highlights.GIT_CONFLICT + elseif git_status == "UU" then + status_symbol = symbols.conflict + status_highlt = highlights.GIT_CONFLICT + change_symbol = symbols.modified + change_highlt = highlights.GIT_CONFLICT + elseif git_status == "AA" then + status_symbol = symbols.conflict + status_highlt = highlights.GIT_CONFLICT + change_symbol = symbols.added + change_highlt = highlights.GIT_CONFLICT + elseif git_status:match("U") then + status_symbol = symbols.conflict + status_highlt = highlights.GIT_CONFLICT + if git_status:match("A") then + change_symbol = symbols.added + elseif git_status:match("D") then + change_symbol = symbols.deleted + end + change_highlt = highlights.GIT_CONFLICT + -- end merge conflict section + elseif git_status:match("M") then + change_symbol = symbols.modified + change_highlt = highlights.GIT_MODIFIED + elseif git_status:match("R") then + change_symbol = symbols.renamed + change_highlt = highlights.GIT_RENAMED + elseif git_status:match("[ACT]") then + change_symbol = symbols.added + change_highlt = highlights.GIT_ADDED + elseif git_status:match("!") then + status_symbol = nil + change_symbol = symbols.ignored + change_highlt = highlights.GIT_IGNORED + elseif git_status:match("D") then + change_symbol = symbols.deleted + change_highlt = highlights.GIT_DELETED + end + + if change_symbol or status_symbol then + local components = {} + if type(change_symbol) == "string" and #change_symbol > 0 then + table.insert(components, { + text = make_two_char(change_symbol), + highlight = change_highlt, + }) + end + if type(status_symbol) == "string" and #status_symbol > 0 then + table.insert(components, { + text = make_two_char(status_symbol), + highlight = status_highlt, + }) + end + return components + else + return { + text = "[" .. git_status .. "]", + highlight = config.highlight or change_highlt, + } + end +end + +---@class neotree.Component.Common.FilteredBy +---@field [1] "filtered_by"? +M.filtered_by = function(_, node, state) + local fby = node.filtered_by + if not state.filtered_items or type(fby) ~= "table" then + return {} + end + repeat + if fby.name then + return { + text = "(hide by name)", + highlight = highlights.HIDDEN_BY_NAME, + } + elseif fby.pattern then + return { + text = "(hide by pattern)", + highlight = highlights.HIDDEN_BY_NAME, + } + elseif fby.gitignored then + return { + text = "(gitignored)", + highlight = highlights.GIT_IGNORED, + } + elseif fby.dotfiles then + return { + text = "(dotfile)", + highlight = highlights.DOTFILE, + } + elseif fby.hidden then + return { + text = "(hidden)", + highlight = highlights.WINDOWS_HIDDEN, + } + end + fby = fby.parent + until not state.filtered_items.children_inherit_highlights or not fby + return {} +end + +---@class (exact) neotree.Component.Common.Icon : neotree.Component +---@field [1] "icon"? +---@field default string The default icon for a node. +---@field folder_empty string The string to display to represent an empty folder. +---@field folder_empty_open string The icon to display to represent an empty but open folder. +---@field folder_open string The icon to display for an open folder. +---@field folder_closed string The icon to display for a closed folder. +---@field provider neotree.IconProvider? + +---@param config neotree.Component.Common.Icon +M.icon = function(config, node, state) + -- calculate default icon + ---@type neotree.Render.Node + local icon = + { text = config.default or " ", highlight = config.highlight or highlights.FILE_ICON } + if node.type == "directory" then + icon.highlight = highlights.DIRECTORY_ICON + if node.loaded and not node:has_children() then + icon.text = not node.empty_expanded and config.folder_empty or config.folder_empty_open + elseif node:is_expanded() then + icon.text = config.folder_open or "-" + else + icon.text = config.folder_closed or "+" + end + end + + -- use icon provider if available + if config.provider then + icon = config.provider(icon, node, state) or icon + end + + local filtered_by = M.filtered_by(config, node, state) + + icon.text = icon.text .. " " -- add padding + icon.highlight = filtered_by.highlight or icon.highlight -- prioritize filtered highlighting + + return icon +end + +---@class (exact) neotree.Component.Common.Modified : neotree.Component +---@field [1] "modified"? +---@field symbol string? + +---@param config neotree.Component.Common.Modified +M.modified = function(config, node, state) + local opened_buffers = state.opened_buffers or {} + local buf_info = utils.index_by_path(opened_buffers, node.path) + + if buf_info and buf_info.modified then + return { + text = (make_two_char(config.symbol) or "[+]"), + highlight = config.highlight or highlights.MODIFIED, + } + else + return {} + end +end + +---@class (exact) neotree.Component.Common.Name : neotree.Component +---@field [1] "name"? +---@field trailing_slash boolean? +---@field use_git_status_colors boolean? +---@field highlight_opened_files boolean|"all"? +---@field right_padding integer? + +---@param config neotree.Component.Common.Name +M.name = function(config, node, state) + local highlight = config.highlight or highlights.FILE_NAME + local text = node.name + if node.type == "directory" then + highlight = highlights.DIRECTORY_NAME + if config.trailing_slash and text ~= "/" then + text = text .. "/" + end + end + + if node:get_depth() == 1 and node.type ~= "message" then + highlight = highlights.ROOT_NAME + if state.current_position == "current" and state.sort and state.sort.label == "Name" then + local icon = state.sort.direction == 1 and "▲" or "▼" + text = text .. " " .. icon + end + else + local filtered_by = M.filtered_by(config, node, state) + highlight = filtered_by.highlight or highlight + if config.use_git_status_colors then + local git_status = state.components.git_status({}, node, state) + if git_status and git_status.highlight then + highlight = git_status.highlight + end + end + end + + local hl_opened = config.highlight_opened_files + if hl_opened then + local opened_buffers = state.opened_buffers or {} + if + (hl_opened == "all" and opened_buffers[node.path]) + or (opened_buffers[node.path] and opened_buffers[node.path].loaded) + then + highlight = highlights.FILE_NAME_OPENED + end + end + + if type(config.right_padding) == "number" then + if config.right_padding > 0 then + text = text .. string.rep(" ", config.right_padding) + end + else + text = text + end + + return { + text = text, + highlight = highlight, + } +end + +---@class (exact) neotree.Component.Common.Indent : neotree.Component +---@field [1] "indent"? +---@field expander_collapsed string? +---@field expander_expanded string? +---@field expander_highlight string? +---@field indent_marker string? +---@field indent_size integer? +---@field last_indent_marker string? +---@field padding integer? +---@field with_expanders boolean? +---@field with_markers boolean? + +---@param config neotree.Component.Common.Indent +M.indent = function(config, node, state) + if not state.skip_marker_at_level then + state.skip_marker_at_level = {} + end + + local strlen = vim.fn.strdisplaywidth + local skip_marker = state.skip_marker_at_level + ---@cast skip_marker -nil + local indent_size = config.indent_size or 2 + local padding = config.padding or 0 + local level = node.level + local with_markers = config.with_markers + local with_expanders = config.with_expanders == nil and file_nesting.is_enabled() + or config.with_expanders + local marker_highlight = config.highlight or highlights.INDENT_MARKER + local expander_highlight = config.expander_highlight or config.highlight or highlights.EXPANDER + + local function get_expander() + if with_expanders and utils.is_expandable(node) then + return node:is_expanded() and (config.expander_expanded or "") + or (config.expander_collapsed or "") + end + end + + if indent_size == 0 or level < 2 or not with_markers then + local len = indent_size * level + padding + local expander = get_expander() + if level == 0 or not expander then + return { + text = string.rep(" ", len), + } + end + return { + text = string.rep(" ", len - strlen(expander) - 1) .. expander .. " ", + highlight = expander_highlight, + } + end + + local indent_marker = config.indent_marker or "│" + local last_indent_marker = config.last_indent_marker or "└" + + skip_marker[level] = node.is_last_child + local indent = {} + if padding > 0 then + table.insert(indent, { text = string.rep(" ", padding) }) + end + + for i = 1, level do + local char = "" + local spaces_count = indent_size + local highlight = nil + + if i > 1 and not skip_marker[i] or i == level then + spaces_count = spaces_count - 1 + char = indent_marker + highlight = marker_highlight + if i == level then + local expander = get_expander() + if expander then + char = expander + highlight = expander_highlight + elseif node.is_last_child then + char = last_indent_marker + spaces_count = spaces_count - (vim.api.nvim_strwidth(last_indent_marker) - 1) + end + end + end + + table.insert(indent, { + text = char .. string.rep(" ", spaces_count), + highlight = highlight, + no_next_padding = true, + }) + end + + return indent +end + +local truncate_string = function(str, max_length) + if #str <= max_length then + return str + end + return str:sub(1, max_length - 1) .. "…" +end + +local get_header = function(state, label, size) + if state.sort and state.sort.label == label then + local icon = state.sort.direction == 1 and "▲" or "▼" + size = size - 2 + ---diagnostic here is wrong, printf has arbitrary args. + ---@diagnostic disable-next-line: redundant-parameter + return vim.fn.printf("%" .. size .. "s %s ", truncate_string(label, size), icon) + end + return vim.fn.printf("%" .. size .. "s ", truncate_string(label, size)) +end + +---@class (exact) neotree.Component.Common.FileSize : neotree.Component +---@field [1] "file_size"? +---@field width integer? + +---@param config neotree.Component.Common.FileSize +M.file_size = function(config, node, state) + -- Root node gets column labels + if node:get_depth() == 1 then + return { + text = get_header(state, "Size", config.width), + highlight = highlights.FILE_STATS_HEADER, + } + end + + local text = "-" + if node.type == "file" then + local stat = utils.get_stat(node) + local size = stat and stat.size or nil + if size then + local success, human = pcall(utils.human_size, size) + if success then + text = human or text + end + end + end + + return { + text = vim.fn.printf("%" .. config.width .. "s ", truncate_string(text, config.width)), + highlight = config.highlight or highlights.FILE_STATS, + } +end + +---@class (exact) neotree.Component.Common._Time : neotree.Component +---@field format neotree.DateFormat +---@field width integer? + +---@param config neotree.Component.Common._Time +local file_time = function(config, node, state, stat_field) + -- Root node gets column labels + if node:get_depth() == 1 then + local label = stat_field + if stat_field == "mtime" then + label = "Last Modified" + elseif stat_field == "birthtime" then + label = "Created" + end + return { + text = get_header(state, label, config.width), + highlight = highlights.FILE_STATS_HEADER, + } + end + + local stat = utils.get_stat(node) + local value = stat and stat[stat_field] + local seconds = value and value.sec or nil + local display = seconds and utils.date(config.format, seconds) or "-" + + return { + text = vim.fn.printf("%" .. config.width .. "s ", truncate_string(display, config.width)), + highlight = config.highlight or highlights.FILE_STATS, + } +end + +---@class (exact) neotree.Component.Common.LastModified : neotree.Component.Common._Time +---@field [1] "last_modified"? + +---@param config neotree.Component.Common.LastModified +M.last_modified = function(config, node, state) + return file_time(config, node, state, "mtime") +end + +---@class (exact) neotree.Component.Common.Created : neotree.Component.Common._Time +---@field [1] "created"? + +---@param config neotree.Component.Common.Created +M.created = function(config, node, state) + return file_time(config, node, state, "birthtime") +end + +---@class (exact) neotree.Component.Common.SymlinkTarget : neotree.Component +---@field [1] "symlink_target"? +---@field text_format string? + +---@param config neotree.Component.Common.SymlinkTarget +M.symlink_target = function(config, node, _) + if node.is_link then + return { + text = string.format(config.text_format or "-> %s", node.link_to), + highlight = config.highlight or highlights.SYMBOLIC_LINK_TARGET, + } + else + return {} + end +end + +---@class (exact) neotree.Component.Common.Type : neotree.Component +---@field [1] "type"? +---@field width integer? + +---@param config neotree.Component.Common.Type +M.type = function(config, node, state) + local text = node.ext or node.type + -- Root node gets column labels + if node:get_depth() == 1 then + return { + text = get_header(state, "Type", config.width), + highlight = highlights.FILE_STATS_HEADER, + } + end + + return { + text = vim.fn.printf("%" .. config.width .. "s ", truncate_string(text, config.width)), + highlight = highlights.FILE_STATS, + } +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/container.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/container.lua new file mode 100644 index 0000000..04e4da9 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/container.lua @@ -0,0 +1,339 @@ +local utils = require("neo-tree.utils") +local renderer = require("neo-tree.ui.renderer") +local highlights = require("neo-tree.ui.highlights") +local log = require("neo-tree.log") + +local M = {} + +local strwidth = vim.api.nvim_strwidth +local calc_rendered_width = function(rendered_item) + local width = 0 + + for _, item in ipairs(rendered_item) do + if item.text then + width = width + strwidth(item.text) + end + end + + return width +end + +local calc_container_width = function(config, node, state, context) + local container_width = 0 + if type(config.width) == "string" then + if config.width == "fit_content" then + container_width = context.max_width + elseif config.width == "100%" then + container_width = context.available_width + elseif config.width:match("^%d+%%$") then + local percent = tonumber(config.width:sub(1, -2)) / 100 + container_width = math.floor(percent * context.available_width) + else + error("Invalid container width: " .. config.width) + end + elseif type(config.width) == "number" then + container_width = config.width + elseif type(config.width) == "function" then + container_width = config.width(node, state) + else + error("Invalid container width: " .. config.width) + end + + if config.min_width then + container_width = math.max(container_width, config.min_width) + end + if config.max_width then + container_width = math.min(container_width, config.max_width) + end + context.container_width = container_width + return container_width +end + +local render_content = function(config, node, state, context) + local window_width = vim.api.nvim_win_get_width(state.winid) + local add_padding = function(rendered_item, should_pad) + for _, data in ipairs(rendered_item) do + if data.text then + local padding = (should_pad and #data.text > 0 and data.text:sub(1, 1) ~= " ") and " " or "" + data.text = padding .. data.text + should_pad = data.text:sub(#data.text) ~= " " + end + end + return should_pad + end + + local max_width = 0 + local grouped_by_zindex = utils.group_by(config.content, "zindex") + + for zindex, items in pairs(grouped_by_zindex) do + local should_pad = { left = false, right = false } + local zindex_rendered = { left = {}, right = {} } + local rendered_width = 0 + + for _, item in ipairs(items) do + repeat + if item.enabled == false then + break + end + local required_width = item.required_width or 0 + if required_width > window_width then + break + end + local rendered_item = renderer.render_component(item, node, state, context.available_width) + if rendered_item then + local align = item.align or "left" + should_pad[align] = add_padding(rendered_item, should_pad[align]) + + vim.list_extend(zindex_rendered[align], rendered_item) + rendered_width = rendered_width + calc_rendered_width(rendered_item) + end + until true + end + + max_width = math.max(max_width, rendered_width) + grouped_by_zindex[zindex] = zindex_rendered + end + + context.max_width = max_width + context.grouped_by_zindex = grouped_by_zindex + return context +end + +local truncate = utils.truncate_by_cell + +---Takes a list of rendered components and truncates them to fit the container width +---@param layer table The list of rendered components. +---@param skip_count number The number of characters to skip from the begining/left. +---@param max_width number The maximum number of characters to return. +local truncate_layer_keep_left = function(layer, skip_count, max_width) + local result = {} + local taken = 0 + local skipped = 0 + for _, item in ipairs(layer) do + local remaining_to_skip = skip_count - skipped + local text_width = strwidth(item.text) + if remaining_to_skip > 0 then + if text_width <= remaining_to_skip then + skipped = skipped + text_width + item.text = "" + else + item.text, text_width = truncate(item.text, text_width - remaining_to_skip, "right") + if text_width > max_width - taken then + item.text, text_width = truncate(item.text, max_width - taken) + end + table.insert(result, item) + taken = taken + text_width + skipped = skipped + remaining_to_skip + end + elseif taken <= max_width then + item.text, text_width = truncate(item.text, max_width - taken) + table.insert(result, item) + taken = taken + text_width + end + end + return result +end + +---Takes a list of rendered components and truncates them to fit the container width +---@param layer table The list of rendered components. +---@param skip_count number The number of characters to skip from the end/right. +---@param max_width number The maximum number of characters to return. +local truncate_layer_keep_right = function(layer, skip_count, max_width) + local result = {} + local taken = 0 + local skipped = 0 + for i = #layer, 1, -1 do + local item = layer[i] + local text_width = strwidth(item.text) + local remaining_to_skip = skip_count - skipped + if remaining_to_skip > 0 then + if text_width <= remaining_to_skip then + skipped = skipped + text_width + item.text = "" + else + item.text, text_width = truncate(item.text, text_width - remaining_to_skip) + if text_width > max_width - taken then + item.text, text_width = truncate(item.text, max_width - taken, "right") + end + table.insert(result, item) + taken = taken + text_width + skipped = skipped + remaining_to_skip + end + elseif taken <= max_width then + if text_width > max_width - taken then + item.text, text_width = truncate(item.text, max_width - taken, "right") + end + table.insert(result, item) + taken = taken + text_width + end + end + return result +end + +local fade_content = function(layer, fade_char_count) + local text = layer[#layer].text + if not text or #text == 0 then + return + end + local hl = layer[#layer].highlight or "Normal" + local fade = { + highlights.get_faded_highlight_group(hl, 0.68), + highlights.get_faded_highlight_group(hl, 0.6), + highlights.get_faded_highlight_group(hl, 0.35), + } + + for i = 3, 1, -1 do + if #text >= i and fade_char_count >= i then + layer[#layer].text = text:sub(1, -i - 1) + for j = i, 1, -1 do + -- force no padding for each faded character + local entry = { text = text:sub(-j, -j), highlight = fade[i - j + 1], no_padding = true } + table.insert(layer, entry) + end + break + end + end +end + +local try_fade_content = function(layer, fade_char_count) + local success, err = pcall(fade_content, layer, fade_char_count) + if not success then + log.debug("Error while trying to fade content: ", err) + end +end + +local merge_content = function(context) + -- Heres the idea: + -- * Starting backwards from the layer with the highest zindex + -- set the left and right tables to the content of the layer + -- * If a layer has more content than will fit, the left side will be truncated. + -- * If the available space is not used up, move on to the next layer + -- * With each subsequent layer, if the length of that layer is greater then the existing + -- length for that side (left or right), then clip that layer and append whatver portion is + -- not covered up to the appropriate side. + -- * Check again to see if we have used up the available width, short circuit if we have. + -- * Repeat until all layers have been merged. + -- * Join the left and right tables together and return. + -- + local remaining_width = context.container_width + local left, right = {}, {} + local left_width, right_width = 0, 0 + local wanted_width = 0 + + if context.left_padding and context.left_padding > 0 then + table.insert(left, { text = string.rep(" ", context.left_padding) }) + remaining_width = remaining_width - context.left_padding + left_width = left_width + context.left_padding + wanted_width = wanted_width + context.left_padding + end + + if context.right_padding and context.right_padding > 0 then + remaining_width = remaining_width - context.right_padding + wanted_width = wanted_width + context.right_padding + end + + local keys = utils.get_keys(context.grouped_by_zindex, true) + if type(keys) ~= "table" then + return {} + end + local i = #keys + while i > 0 do + local key = keys[i] + local layer = context.grouped_by_zindex[key] + i = i - 1 + + if utils.truthy(layer.right) then + local width = calc_rendered_width(layer.right) + wanted_width = wanted_width + width + if remaining_width > 0 then + context.has_right_content = true + if width > remaining_width then + local truncated = truncate_layer_keep_right(layer.right, right_width, remaining_width) + vim.list_extend(right, truncated) + remaining_width = 0 + else + remaining_width = remaining_width - width + vim.list_extend(right, layer.right) + right_width = right_width + width + end + end + end + + if utils.truthy(layer.left) then + local width = calc_rendered_width(layer.left) + wanted_width = wanted_width + width + if remaining_width > 0 then + if width > remaining_width then + local truncated = truncate_layer_keep_left(layer.left, left_width, remaining_width) + if context.enable_character_fade then + try_fade_content(truncated, 3) + end + vim.list_extend(left, truncated) + remaining_width = 0 + else + remaining_width = remaining_width - width + if context.enable_character_fade and not context.auto_expand_width then + local fade_chars = 3 - remaining_width + if fade_chars > 0 then + try_fade_content(layer.left, fade_chars) + end + end + vim.list_extend(left, layer.left) + left_width = left_width + width + end + end + end + + if remaining_width == 0 and not context.auto_expand_width then + i = 0 + break + end + end + + if remaining_width > 0 and #right > 0 then + table.insert(left, { text = string.rep(" ", remaining_width) }) + end + + local result = {} + vim.list_extend(result, left) + + -- we do not pad between left and right side + if #right >= 1 then + right[1].no_padding = true + end + + vim.list_extend(result, right) + context.merged_content = result + log.trace("wanted width: ", wanted_width, " actual width: ", context.container_width) + context.wanted_width = math.max(wanted_width, context.wanted_width) +end + +---@param config neotree.Component.Common.Container +M.render = function(config, node, state, available_width) + local context = { + wanted_width = 0, + max_width = 0, + grouped_by_zindex = {}, + available_width = available_width, + left_padding = config.left_padding, + right_padding = config.right_padding, + enable_character_fade = config.enable_character_fade, + auto_expand_width = state.window.auto_expand_width and state.window.position ~= "float", + } + + render_content(config, node, state, context) + calc_container_width(config, node, state, context) + merge_content(context) + + if context.has_right_content then + state.has_right_content = true + end + + -- we still want padding between this container and the previous component + if #context.merged_content > 0 then + context.merged_content[1].no_padding = false + end + return context.merged_content, context.wanted_width +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/file-items.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/file-items.lua new file mode 100644 index 0000000..c096873 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/file-items.lua @@ -0,0 +1,341 @@ +local file_nesting = require("neo-tree.sources.common.file-nesting") +local utils = require("neo-tree.utils") +local log = require("neo-tree.log") +local uv = vim.uv or vim.loop + +---@type neotree.Config.SortFunction +local function sort_items(a, b) + if a.type == b.type then + return a.path < b.path + else + return a.type < b.type + end +end + +---@type neotree.Config.SortFunction +local function sort_items_case_insensitive(a, b) + if a.type == b.type then + return a.path:lower() < b.path:lower() + else + return a.type < b.type + end +end + +---Creates a sort function the will sort by the values returned by the field provider. +---@param field_provider neotree.Internal.SortFieldProvider a function that takes an item and returns a value to sort by. +---@param fallback_sort_function neotree.Config.SortFunction a sort function to use if the field provider returns the same value for both items. +---@return neotree.Config.SortFunction +local function make_sort_function(field_provider, fallback_sort_function, direction) + return function(a, b) + if a.type == b.type then + local a_field = field_provider(a) + local b_field = field_provider(b) + if a_field == b_field then + return fallback_sort_function(a, b) + else + if direction < 0 then + return a_field > b_field + else + return a_field < b_field + end + end + else + return a.type < b.type + end + end +end + +---@param func neotree.Config.SortFunction? +---@return boolean +local function sort_function_is_valid(func) + if func == nil then + return false + end + + local a = { type = "dir", path = "foo" } + local b = { type = "dir", path = "baz" } + + local success, result = pcall(func, a, b) + if success and type(result) == "boolean" then + return true + end + + log.error("sort function isn't valid ", result) + return false +end + +---@param tbl table +---@param sort_func neotree.Config.SortFunction? +---@param field_provider neotree.Internal.SortFieldProvider? +---@param direction? 1|0 +local function deep_sort(tbl, sort_func, field_provider, direction) + if sort_func == nil then + local config = require("neo-tree").config + if sort_function_is_valid(config.sort_function) then + sort_func = config.sort_function + elseif config.sort_case_insensitive then + sort_func = sort_items_case_insensitive + else + sort_func = sort_items + end + ---@cast sort_func -nil + if field_provider ~= nil then + sort_func = make_sort_function(field_provider, sort_func, direction) + end + end + table.sort(tbl, sort_func) + for _, item in pairs(tbl) do + if item.type == "directory" or item.children ~= nil then + deep_sort(item.children, sort_func) + end + end +end + +---@param state neotree.State +local advanced_sort = function(tbl, state) + local sort_func = state.sort_function_override + local field_provider = state.sort_field_provider + local direction = state.sort and state.sort.direction or 1 + deep_sort(tbl, sort_func, field_provider, direction) +end + +local create_item, set_parents + +---@alias neotree.Filetype +---|"file" +---|"link" +---|"directory" +---|"unknown" + +---@class neotree.FileItemFilters +---@field never_show boolean? +---@field always_show boolean? +---@field name boolean? +---@field pattern boolean? +---@field dotfiles boolean? +---@field hidden boolean? +---@field gitignored boolean? +---@field parent neotree.FileItemFilters? +---@field show_gitignored boolean? + +---@class (exact) neotree.FileItemExtra +---@field status string? Git status + +---@class (exact) neotree.FileItem +---@field id string +---@field name string +---@field parent_path string? +---@field path string +---@field type neotree.Filetype|string +---@field is_reveal_target boolean +---@field contains_reveal_target boolean +---@field filtered_by neotree.FileItemFilters? +---@field extra neotree.FileItemExtra? +---@field status string? Git status +---@field is_nested boolean? + +---@class (exact) neotree.FileItem.File : neotree.FileItem +---@field children table? +---@field nesting_callback neotree.filenesting.Callback +---@field base string +---@field ext string +---@field exts string +---@field name_lcase string + +---@class (exact) neotree.FileItem.Link : neotree.FileItem +---@field is_link boolean +---@field link_to string? + +---@class (exact) neotree.FileItem.Directory : neotree.FileItem +---@field children table +---@field loaded boolean +---@field search_pattern string? + +---@param context neotree.FileItemContext +---@param path string +---@param _type neotree.Filetype? +---@param bufnr integer? +---@return neotree.FileItem +function create_item(context, path, _type, bufnr) + local parent_path, name = utils.split_path(utils.normalize_path(path)) + name = name or "" + local id = path + if path == "[No Name]" and bufnr then + parent_path = context.state.path + name = "[No Name]" + id = tostring(bufnr) + else + -- avoid creating duplicate items + if context.folders[path] or context.nesting[path] or context.item_exists[path] then + return context.folders[path] or context.nesting[path] or context.item_exists[path] + end + end + + if _type == nil then + local stat = uv.fs_stat(path) + _type = stat and stat.type or "unknown" + end + local is_reveal_target = (path == context.path_to_reveal) + ---@type neotree.FileItem + local item = { + id = id, + name = name, + parent_path = parent_path, + path = path, + type = _type, + is_reveal_target = is_reveal_target, + contains_reveal_target = is_reveal_target and utils.is_subpath(path, context.path_to_reveal), + } + if utils.is_windows then + if vim.fn.getftype(path) == "link" then + item.type = "link" + end + end + if item.type == "link" then + ---@cast item neotree.FileItem.Link + item.is_link = true + item.link_to = uv.fs_realpath(path) + if item.link_to ~= nil then + item.type = uv.fs_stat(item.link_to).type + end + end + if item.type == "directory" then + ---@cast item neotree.FileItem.Directory + item.children = {} + item.loaded = false + context.folders[path] = item + if context.state.search_pattern then + table.insert(context.state.default_expanded_nodes, item.id) + end + else + ---@cast item neotree.FileItem.File + item.base = item.name:match("^([-_,()%s%w%i]+)%.") + item.ext = item.name:match("%.([-_,()%s%w%i]+)$") + item.exts = item.name:match("^[-_,()%s%w%i]+%.(.*)") + item.name_lcase = item.name:lower() + + local nesting_callback = file_nesting.get_nesting_callback(item) + if nesting_callback ~= nil then + item.children = {} + item.nesting_callback = nesting_callback + context.nesting[path] = item + end + end + + local state = assert(context.state) + local f = state.filtered_items + local is_not_root = not utils.is_subpath(path, context.state.path) + if f and is_not_root then + if f.never_show[name] then + item.filtered_by = item.filtered_by or {} + item.filtered_by.never_show = true + else + if utils.is_filtered_by_pattern(f.never_show_by_pattern, path, name) then + item.filtered_by = item.filtered_by or {} + item.filtered_by.never_show = true + end + end + if f.always_show[name] then + item.filtered_by = item.filtered_by or {} + item.filtered_by.always_show = true + else + if utils.is_filtered_by_pattern(f.always_show_by_pattern, path, name) then + item.filtered_by = item.filtered_by or {} + item.filtered_by.always_show = true + end + end + if f.hide_by_name[name] then + item.filtered_by = item.filtered_by or {} + item.filtered_by.name = true + end + if utils.is_filtered_by_pattern(f.hide_by_pattern, path, name) then + item.filtered_by = item.filtered_by or {} + item.filtered_by.pattern = true + end + if f.hide_dotfiles and string.sub(name, 1, 1) == "." then + item.filtered_by = item.filtered_by or {} + item.filtered_by.dotfiles = true + end + if f.hide_hidden and utils.is_hidden(path) then + item.filtered_by = item.filtered_by or {} + item.filtered_by.hidden = true + end + -- NOTE: git_ignored logic moved to job_complete + end + + set_parents(context, item) + if context.all_items == nil then + context.all_items = {} + end + if is_not_root then + table.insert(context.all_items, item) + end + return item +end + +-- function to set (or create) parent folder +---@param context neotree.FileItemContext +---@param item neotree.FileItem +function set_parents(context, item) + -- we can get duplicate items if we navigate up with open folders + -- this is probably hacky, but it works + if context.item_exists[item.id] then + return + end + if not item.parent_path then + return + end + + local parent = context.folders[item.parent_path] + if not utils.truthy(item.parent_path) then + return + end + if parent == nil then + local success + success, parent = pcall(create_item, context, item.parent_path, "directory") + if not success then + log.error("error creating item for ", item.parent_path) + end + ---@cast parent neotree.FileItem.Directory + context.folders[parent.id] = parent + set_parents(context, parent) + end + table.insert(parent.children, item) + context.item_exists[item.id] = true + + if not item.filtered_by and parent.filtered_by then + item.filtered_by = { + parent = parent.filtered_by, + } + end +end + +---@class (exact) neotree.FileItemContext +---@field state neotree.State? +---@field folders table +---@field nesting neotree.FileItem[] +---@field item_exists table +---@field all_items table +---@field path_to_reveal string? + +---Create context to be used in other file-items functions. +---@param state neotree.State? The state of the file-items. +---@return neotree.FileItemContext +local create_context = function(state) + local context = {} + -- Make the context a weak table so that it can be garbage collected + --setmetatable(context, { __mode = 'v' }) + context.state = state + context.folders = {} + context.nesting = {} + context.item_exists = {} + context.all_items = {} + return context +end + +return { + create_context = create_context, + create_item = create_item, + deep_sort = deep_sort, + advanced_sort = advanced_sort, +} diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/file-nesting.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/file-nesting.lua new file mode 100644 index 0000000..85597b3 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/file-nesting.lua @@ -0,0 +1,324 @@ +local utils = require("neo-tree.utils") +local globtopattern = require("neo-tree.sources.filesystem.lib.globtopattern") +local log = require("neo-tree.log") + +-- File nesting a la JetBrains (#117). +local M = {} + +---@alias neotree.filenesting.Callback fun(item: table, siblings: table[], rule: neotree.filenesting.Rule): neotree.filenesting.Matches + +---@class neotree.filenesting.Matcher +---@field rules table|neotree.filenesting.Rule[] +---@field get_children neotree.filenesting.Callback +---@field get_nesting_callback fun(item: table): neotree.filenesting.Callback|nil A callback that returns all the files + +local DEFAULT_PATTERN_PRIORITY = 100 +---@class neotree.filenesting.Rule +---@field priority number? Default is 100. Higher is prioritized. +---@field _priority number The internal priority, lower is prioritized. Determined through priority and the key for the rule at setup. + +---@class neotree.filenesting.Rule.Pattern : neotree.filenesting.Rule +---@field files string[] +---@field files_exact string[]? +---@field files_glob string[]? +---@field ignore_case boolean? Default is false +---@field pattern string + +---@class neotree.filenesting.Matcher.Pattern : neotree.filenesting.Matcher +---@field rules neotree.filenesting.Rule.Pattern[] +local pattern_matcher = { + rules = {}, +} + +---@class neotree.filenesting.Rule.Extension : neotree.filenesting.Rule +---@field [integer] string + +---@class neotree.filenesting.Matcher.Extension : neotree.filenesting.Matcher +---@field rules table +local extension_matcher = { + rules = {}, +} + +local matchers = { + pattern = pattern_matcher, + exts = extension_matcher, +} + +---@class neotree.filenesting.Matches +---@field priority number +---@field parent table +---@field children table[] + +extension_matcher.get_nesting_callback = function(item) + local rule = extension_matcher.rules[item.exts] + if utils.truthy(rule) then + return function(inner_item, siblings) + return { + parent = inner_item, + children = extension_matcher.get_children(inner_item, siblings, rule), + priority = rule._priority, + } + end + end + return nil +end + +---@type neotree.filenesting.Callback +extension_matcher.get_children = function(item, siblings, rule) + local matching_files = {} + if siblings == nil then + return matching_files + end + for _, ext in pairs(rule) do + for _, sibling in pairs(siblings) do + if + sibling.id ~= item.id + and sibling.exts == ext + and item.base .. "." .. ext == sibling.name + then + table.insert(matching_files, sibling) + end + end + end + ---@type neotree.filenesting.Matches + return matching_files +end + +pattern_matcher.get_nesting_callback = function(item) + ---@type neotree.filenesting.Rule.Pattern[] + local matching_rules = {} + for _, rule in ipairs(pattern_matcher.rules) do + if item.name:match(rule.pattern) then + table.insert(matching_rules, rule) + end + end + + if #matching_rules > 0 then + return function(inner_item, siblings) + local match_set = {} + ---@type neotree.filenesting.Matches[] + local all_item_matches = {} + for _, rule in ipairs(matching_rules) do + ---@type neotree.filenesting.Matches + local item_matches = { + priority = rule._priority, + parent = inner_item, + children = {}, + } + local matched_siblings = pattern_matcher.get_children(inner_item, siblings, rule) + for _, match in ipairs(matched_siblings) do + -- Use file path as key to prevent duplicates + if not match_set[match.id] then + match_set[match.id] = true + table.insert(item_matches.children, match) + end + end + table.insert(all_item_matches, item_matches) + end + + return all_item_matches + end + end + return nil +end + +local pattern_matcher_types = { + files_glob = { + get_pattern = function(pattern) + return globtopattern.globtopattern(pattern) + end, + match = function(filename, pattern) + return filename:match(pattern) + end, + }, + files_exact = { + get_pattern = function(pattern) + return pattern + end, + match = function(filename, pattern) + return filename == pattern + end, + }, +} + +---@type neotree.filenesting.Callback +pattern_matcher.get_children = function(item, siblings, rule) + local matching_files = {} + if siblings == nil then + return matching_files + end + + for type, type_functions in pairs(pattern_matcher_types) do + for _, pattern in pairs(rule[type] or {}) do + repeat + ---@cast rule neotree.filenesting.Rule.Pattern + local item_name = rule.ignore_case and item.name:lower() or item.name + + local success, replaced_pattern = pcall(string.gsub, item_name, rule.pattern, pattern) + if not success then + log.error("Error using file glob '" .. pattern .. "'; Error: " .. replaced_pattern) + break + end + for _, sibling in pairs(siblings) do + if sibling.id ~= item.id then + local sibling_name = rule.ignore_case and sibling.name:lower() or sibling.name + local glob_or_file = type_functions.get_pattern(replaced_pattern) + if type_functions.match(sibling_name, glob_or_file) then + table.insert(matching_files, sibling) + end + end + end + until true + end + end + return matching_files +end + +---@type neotree.filenesting.Matcher[] +local enabled_matchers = {} + +function M.is_enabled() + return not vim.tbl_isempty(enabled_matchers) +end + +function M.nest_items(context) + if not M.is_enabled() or vim.tbl_isempty(context.nesting or {}) then + return + end + + -- First collect all nesting relationships + ---@type neotree.filenesting.Matches[] + local nesting_relationships = {} + for _, parent in pairs(context.nesting) do + local siblings = context.folders[parent.parent_path].children + vim.list_extend(nesting_relationships, parent.nesting_callback(parent, siblings)) + end + + table.sort(nesting_relationships, function(a, b) + if a.priority == b.priority then + return a.parent.id < b.parent.id + end + return a.priority < b.priority + end) + + -- Then apply them in order + for _, relationship in ipairs(nesting_relationships) do + local folder = context.folders[relationship.parent.parent_path] + for _, sibling in ipairs(relationship.children) do + if not sibling.is_nested then + table.insert(relationship.parent.children, sibling) + sibling.is_nested = true + sibling.nesting_parent = relationship.parent + + if folder ~= nil then + for index, file_to_check in ipairs(folder.children) do + if file_to_check.id == sibling.id then + table.remove(folder.children, index) + break + end + end + end + end + end + end +end + +function M.get_nesting_callback(item) + local cbs = {} + for _, matcher in ipairs(enabled_matchers) do + local callback = matcher.get_nesting_callback(item) + if callback ~= nil then + table.insert(cbs, callback) + end + end + if #cbs <= 1 then + return cbs[1] + else + return function(...) + local res = {} + for _, cb in ipairs(cbs) do + vim.list_extend(res, cb(...)) + end + return res + end + end +end + +local function is_glob(str) + local test = str:gsub("\\[%*%?%[%]]", "") + local pos, _ = test:find("*") + return pos ~= nil +end + +local function case_insensitive_pattern(pattern) + -- find an optional '%' (group 1) followed by any character (group 2) + local p = pattern:gsub("(%%?)(.)", function(percent, letter) + if percent ~= "" or not letter:match("%a") then + -- if the '%' matched, or `letter` is not a letter, return "as is" + return percent .. letter + else + -- else, return a case-insensitive character class of the matched letter + return string.format("[%s%s]", letter:lower(), letter:upper()) + end + end) + + return p +end + +---Setup the module with the given config +---@param config table +function M.setup(config) + config = config or {} + enabled_matchers = {} + local real_priority = 0 + for _, m in pairs(matchers) do + m.rules = {} + end + + for key, rule in + utils.spairs(config, function(a, b) + -- Organize by priority (descending) or by key (ascending) + local a_prio = config[a].priority or DEFAULT_PATTERN_PRIORITY + local b_prio = config[b].priority or DEFAULT_PATTERN_PRIORITY + if a_prio == b_prio then + return a < b + end + return a_prio > b_prio + end) + do + rule.priority = rule.priority or DEFAULT_PATTERN_PRIORITY + rule._priority = real_priority + real_priority = real_priority + 1 + if rule.pattern then + ---@cast rule neotree.filenesting.Rule.Pattern + rule.ignore_case = rule.ignore_case or false + if rule.ignore_case then + rule.pattern = case_insensitive_pattern(rule.pattern) + end + rule.files_glob = {} + rule.files_exact = {} + for _, glob in pairs(rule.files) do + if rule.ignore_case then + glob = glob:lower() + end + local replaced = glob:gsub("%%%d+", "") + if is_glob(replaced) then + table.insert(rule.files_glob, glob) + else + table.insert(rule.files_exact, glob) + end + end + -- priority does matter for pattern.rules + table.insert(matchers.pattern.rules, rule) + else + ---@cast rule neotree.filenesting.Rule.Extension + matchers.exts.rules[key] = rule + end + end + + enabled_matchers = vim.tbl_filter(function(m) + return not vim.tbl_isempty(m.rules) + end, matchers) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/filters/filter_fzy.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/filters/filter_fzy.lua new file mode 100644 index 0000000..18e92a1 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/filters/filter_fzy.lua @@ -0,0 +1,249 @@ +-- The lua implementation of the fzy string matching algorithm +-- credits to: https://github.com/swarn/fzy-lua +--[[ +The MIT License (MIT) + +Copyright (c) 2020 Seth Warn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +--]] +-- modified by: @pysan3 (2023) + +local SCORE_GAP_LEADING = -0.005 +local SCORE_GAP_TRAILING = -0.005 +local SCORE_GAP_INNER = -0.01 +local SCORE_MATCH_CONSECUTIVE = 1.0 +local SCORE_MATCH_SLASH = 0.9 +local SCORE_MATCH_WORD = 0.8 +local SCORE_MATCH_CAPITAL = 0.7 +local SCORE_MATCH_DOT = 0.6 +local SCORE_MAX = math.huge +local SCORE_MIN = -math.huge +local MATCH_MAX_LENGTH = 1024 + +local M = {} + +-- Return `true` if `needle` is a subsequence of `haystack`. +function M.has_match(needle, haystack, case_sensitive) + if not case_sensitive then + needle = string.lower(needle) + haystack = string.lower(haystack) + end + + ---@type integer? + local j = 1 + for i = 1, string.len(needle) do + j = string.find(haystack, needle:sub(i, i), j, true) + if not j then + return false + else + j = j + 1 + end + end + + return true +end + +local function is_lower(c) + return c:match("%l") +end + +local function is_upper(c) + return c:match("%u") +end + +local function precompute_bonus(haystack) + local match_bonus = {} + + local last_char = "/" + for i = 1, string.len(haystack) do + local this_char = haystack:sub(i, i) + if last_char == "/" or last_char == "\\" then + match_bonus[i] = SCORE_MATCH_SLASH + elseif last_char == "-" or last_char == "_" or last_char == " " then + match_bonus[i] = SCORE_MATCH_WORD + elseif last_char == "." then + match_bonus[i] = SCORE_MATCH_DOT + elseif is_lower(last_char) and is_upper(this_char) then + match_bonus[i] = SCORE_MATCH_CAPITAL + else + match_bonus[i] = 0 + end + + last_char = this_char + end + + return match_bonus +end + +local function compute(needle, haystack, D, T, case_sensitive) + -- Note that the match bonuses must be computed before the arguments are + -- converted to lowercase, since there are bonuses for camelCase. + local match_bonus = precompute_bonus(haystack) + local n = string.len(needle) + local m = string.len(haystack) + + if not case_sensitive then + needle = string.lower(needle) + haystack = string.lower(haystack) + end + + -- Because lua only grants access to chars through substring extraction, + -- get all the characters from the haystack once now, to reuse below. + local haystack_chars = {} + for i = 1, m do + haystack_chars[i] = haystack:sub(i, i) + end + + for i = 1, n do + D[i] = {} + T[i] = {} + + local prev_score = SCORE_MIN + local gap_score = i == n and SCORE_GAP_TRAILING or SCORE_GAP_INNER + local needle_char = needle:sub(i, i) + + for j = 1, m do + if needle_char == haystack_chars[j] then + local score = SCORE_MIN + if i == 1 then + score = ((j - 1) * SCORE_GAP_LEADING) + match_bonus[j] + elseif j > 1 then + local a = T[i - 1][j - 1] + match_bonus[j] + local b = D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE + score = math.max(a, b) + end + D[i][j] = score + prev_score = math.max(score, prev_score + gap_score) + T[i][j] = prev_score + else + D[i][j] = SCORE_MIN + prev_score = prev_score + gap_score + T[i][j] = prev_score + end + end + end +end + +-- Compute a matching score for two strings. +-- +-- Where `needle` is a subsequence of `haystack`, this returns a score +-- measuring the quality of their match. Better matches get higher scores. +-- +-- `needle` must be a subsequence of `haystack`, the result is undefined +-- otherwise. Call `has_match()` before calling `score`. +-- +-- returns `get_score_min()` where a or b are longer than `get_max_length()` +-- +-- returns `get_score_min()` when a or b are empty strings. +-- +-- returns `get_score_max()` when a and b are the same string. +-- +-- When the return value is not covered by the above rules, it is a number +-- in the range (`get_score_floor()`, `get_score_ceiling()`) +function M.score(needle, haystack, case_sensitive) + local n = string.len(needle) + local m = string.len(haystack) + + if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > MATCH_MAX_LENGTH then + return SCORE_MIN + elseif n == m then + return SCORE_MAX + else + local D = {} + local T = {} + compute(needle, haystack, D, T, case_sensitive) + return T[n][m] + end +end + +-- Find the locations where fzy matched a string. +-- +-- Returns {score, indices}, where indices is an array showing where each +-- character of the needle matches the haystack in the best match. +function M.score_and_positions(needle, haystack, case_sensitive) + local n = string.len(needle) + local m = string.len(haystack) + + if n == 0 or m == 0 or m > MATCH_MAX_LENGTH or n > MATCH_MAX_LENGTH then + return SCORE_MIN, {} + elseif n == m then + local consecutive = {} + for i = 1, n do + consecutive[i] = i + end + return SCORE_MAX, consecutive + end + + local D = {} + local T = {} + compute(needle, haystack, D, T, case_sensitive) + + local positions = {} + local match_required = false + local j = m + for i = n, 1, -1 do + while j >= 1 do + if D[i][j] ~= SCORE_MIN and (match_required or D[i][j] == T[i][j]) then + match_required = (i ~= 1) + and (j ~= 1) + and (T[i][j] == D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE) + positions[i] = j + j = j - 1 + break + else + j = j - 1 + end + end + end + + return T[n][m], positions +end + +-- Return only the positions of a match. +function M.positions(needle, haystack, case_sensitive) + local _, positions = M.score_and_positions(needle, haystack, case_sensitive) + return positions +end + +function M.get_score_min() + return SCORE_MIN +end + +function M.get_score_max() + return SCORE_MAX +end + +function M.get_max_length() + return MATCH_MAX_LENGTH +end + +function M.get_score_floor() + return MATCH_MAX_LENGTH * SCORE_GAP_INNER +end + +function M.get_score_ceiling() + return MATCH_MAX_LENGTH * SCORE_MATCH_CONSECUTIVE +end + +function M.get_implementation_name() + return "lua" +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/filters/init.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/filters/init.lua new file mode 100644 index 0000000..b0c8066 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/filters/init.lua @@ -0,0 +1,355 @@ +---A generalization of the filter functionality to directly filter the +---source tree instead of relying on pre-filtered data, which is specific +---to the filesystem source. +local Input = require("nui.input") +local event = require("nui.utils.autocmd").event +local popups = require("neo-tree.ui.popups") +local renderer = require("neo-tree.ui.renderer") +local utils = require("neo-tree.utils") +local compat = require("neo-tree.utils._compat") +local log = require("neo-tree.log") +local manager = require("neo-tree.sources.manager") +local fzy = require("neo-tree.sources.common.filters.filter_fzy") + +local M = {} + +---Reset the current filter to the empty string. +---@param state neotree.State +---@param refresh boolean? whether to refresh the source tree +---@param open_current_node boolean? whether to open the current node +local reset_filter = function(state, refresh, open_current_node) + log.trace("reset_search") + if refresh == nil then + refresh = true + end + + -- Cancel any pending search + require("neo-tree.sources.filesystem.lib.filter_external").cancel() + + -- reset search state + if state.open_folders_before_search then + state.force_open_folders = vim.deepcopy(state.open_folders_before_search, compat.noref()) + else + state.force_open_folders = nil + end + state.open_folders_before_search = nil + state.search_pattern = nil + + if open_current_node then + local success, node = pcall(state.tree.get_node, state.tree) + if success and node then + local id = node:get_id() + renderer.position.set(state, id) + id = utils.remove_trailing_slash(id) + manager.navigate(state, nil, id, utils.wrap(pcall, renderer.focus_node, state, id, false)) + end + elseif refresh then + manager.navigate(state) + else + state.tree = vim.deepcopy(state.orig_tree) + end + state.orig_tree = nil +end + +---Show the filtered tree +---@param state any +---@param do_not_focus_window boolean? whether to focus the window +local show_filtered_tree = function(state, do_not_focus_window) + state.tree = vim.deepcopy(state.orig_tree) + state.tree:get_nodes()[1].search_pattern = state.search_pattern + local max_score, max_id = fzy.get_score_min(), nil + local function filter_tree(node_id) + local node = state.tree:get_node(node_id) + local path = node.extra.search_path or node.path + + local should_keep = fzy.has_match(state.search_pattern, path) + if should_keep then + local score = fzy.score(state.search_pattern, path) + node.extra.fzy_score = score + if score > max_score then + max_score = score + max_id = node_id + end + end + + if node:has_children() then + for _, child_id in ipairs(node:get_child_ids()) do + should_keep = filter_tree(child_id) or should_keep + end + end + if not should_keep then + state.tree:remove_node(node_id) -- TODO: this might not be efficient + end + return should_keep + end + if #state.search_pattern > 0 then + for _, root in ipairs(state.tree:get_nodes()) do + filter_tree(root:get_id()) + end + end + manager.redraw(state.name) + if max_id then + renderer.focus_node(state, max_id, do_not_focus_window) + end +end + +---Main entry point for the filter functionality. +---This will display a filter input popup and filter the source tree on change and on submit +---@param state neotree.State the source state +---@param search_as_you_type boolean? whether to filter as you type or only on submit +---@param keep_filter_on_submit boolean? whether to keep the filter on or reset it +M.show_filter = function(state, search_as_you_type, keep_filter_on_submit) + local winid = vim.api.nvim_get_current_win() + local height = vim.api.nvim_win_get_height(winid) + local scroll_padding = 3 + + -- setup the input popup options + local popup_msg = "Search:" + if search_as_you_type then + popup_msg = "Filter:" + end + if state.config.title then + popup_msg = state.config.title + end + + local width = vim.fn.winwidth(0) - 2 + local row = height - 3 + if state.current_position == "float" then + scroll_padding = 0 + width = vim.fn.winwidth(winid) + row = height - 2 + vim.api.nvim_win_set_height(winid, row) + end + + state.orig_tree = vim.deepcopy(state.tree) + + local popup_options = popups.popup_options(popup_msg, width, { + relative = "win", + winid = winid, + position = { + row = row, + col = 0, + }, + size = width, + }) + + local has_pre_search_folders = utils.truthy(state.open_folders_before_search) + if not has_pre_search_folders then + log.trace("No search or pre-search folders, recording pre-search folders now") + state.open_folders_before_search = renderer.get_expanded_nodes(state.tree) + end + + local waiting_for_default_value = utils.truthy(state.search_pattern) + local input = Input(popup_options, { + prompt = " ", + default_value = state.search_pattern, + on_submit = function(value) + if value == "" then + reset_filter(state) + return + end + if search_as_you_type and not keep_filter_on_submit then + reset_filter(state, true, true) + return + end + -- do the search + state.search_pattern = value + show_filtered_tree(state, false) + end, + --this can be bad in a deep folder structure + on_change = function(value) + if not search_as_you_type then + return + end + -- apparently when a default value is set, on_change fires for every character + if waiting_for_default_value then + if #value < #state.search_pattern then + return + end + waiting_for_default_value = false + end + if value == state.search_pattern or value == nil then + return + end + + -- finally do the search + log.trace("Setting search in on_change to: " .. value) + state.search_pattern = value + local len_to_delay = { [0] = 500, 500, 400, 200 } + local delay = len_to_delay[#value] or 100 + + utils.debounce(state.name .. "_filter", function() + show_filtered_tree(state, true) + end, delay, utils.debounce_strategy.CALL_LAST_ONLY) + end, + }) + + input:mount() + + local restore_height = vim.schedule_wrap(function() + if vim.api.nvim_win_is_valid(winid) then + vim.api.nvim_win_set_height(winid, height) + end + end) + + ---@alias neotree.FuzzyFinder.BuiltinCommandNames + ---|"move_cursor_down" + ---|"move_cursor_up" + ---|"close" + ---|"close_clear_filter" + ---|"close_keep_filter" + ---|neotree.FuzzyFinder.FalsyMappingNames + + ---@alias neotree.FuzzyFinder.CommandFunction fun(state: neotree.State, scroll_padding: integer):string? + + ---@class neotree.FuzzyFinder.BuiltinCommands + ---@field [string] neotree.FuzzyFinder.CommandFunction? + local cmds + cmds = { + move_cursor_down = function(state_, scroll_padding_) + renderer.focus_node(state_, nil, true, 1, scroll_padding_) + end, + + move_cursor_up = function(state_, scroll_padding_) + renderer.focus_node(state_, nil, true, -1, scroll_padding_) + vim.cmd("redraw!") + end, + + close = function(_state) + vim.cmd("stopinsert") + input:unmount() + if utils.truthy(_state.search_pattern) then + reset_filter(_state, true) + end + restore_height() + end, + + close_keep_filter = function(_state, _scroll_padding) + log.info("Persisting the search filter") + keep_filter_on_submit = true + cmds.close(_state, _scroll_padding) + end, + close_clear_filter = function(_state, _scroll_padding) + log.info("Clearing the search filter") + keep_filter_on_submit = false + cmds.close(_state, _scroll_padding) + end, + } + + M.setup_hooks(input, cmds, state, scroll_padding) + M.setup_mappings(input, cmds, state, scroll_padding) +end + +---@param input NuiInput +---@param cmds neotree.FuzzyFinder.BuiltinCommands +---@param state neotree.State +---@param scroll_padding integer +function M.setup_hooks(input, cmds, state, scroll_padding) + input:on( + { event.BufLeave, event.BufDelete }, + utils.wrap(cmds.close, state, scroll_padding), + { once = true } + ) + + -- hacky bugfix for quitting from the filter window + input:on("QuitPre", function() + if vim.api.nvim_get_current_win() ~= input.winid then + return + end + ---'confirm' can cause blocking user input on exit, so this hack disables it. + local old_confirm = vim.o.confirm + vim.o.confirm = false + vim.schedule(function() + vim.o.confirm = old_confirm + end) + end) +end + +---@enum neotree.FuzzyFinder.FalsyMappingNames +M._falsy_mapping_names = { "noop", "none" } + +---@alias neotree.FuzzyFinder.CommandOrName neotree.FuzzyFinder.CommandFunction|neotree.FuzzyFinder.BuiltinCommandNames + +---@class neotree.FuzzyFinder.VerboseCommand +---@field [1] neotree.FuzzyFinder.Command +---@field [2] vim.keymap.set.Opts? +---@field raw boolean? + +---@alias neotree.FuzzyFinder.Command neotree.FuzzyFinder.CommandOrName|neotree.FuzzyFinder.VerboseCommand|string + +---@class neotree.FuzzyFinder.SimpleMappings : neotree.SimpleMappings +---@field [string] neotree.FuzzyFinder.Command? + +---@class neotree.Config.FuzzyFinder.Mappings : neotree.FuzzyFinder.SimpleMappings, neotree.Mappings +---@field [integer] table + +---@param input NuiInput +---@param cmds neotree.FuzzyFinder.BuiltinCommands +---@param state neotree.State +---@param scroll_padding integer +---@param mappings neotree.FuzzyFinder.SimpleMappings +---@param mode string +local function apply_simple_mappings(input, cmds, state, scroll_padding, mode, mappings) + ---@param command neotree.FuzzyFinder.CommandFunction + ---@return function + local function setup_command(command) + return utils.wrap(command, state, scroll_padding) + end + for lhs, rhs in pairs(mappings) do + if type(lhs) == "string" then + ---@cast rhs neotree.FuzzyFinder.Command + local cmd, raw, opts + if type(rhs) == "table" then + ---type doesn't narrow properly + ---@cast rhs -neotree.FuzzyFinder.FalsyMappingNames + raw = rhs.raw + opts = vim.deepcopy(rhs) + opts[1] = nil + opts.raw = nil + cmd = rhs[1] + else + ---type also doesn't narrow properly + ---@cast rhs -neotree.FuzzyFinder.VerboseCommand + cmd = rhs + end + + local cmdtype = type(cmd) + if cmdtype == "string" then + if raw then + input:map(mode, lhs, cmd, opts) + else + local command = cmds[cmd] + if command then + input:map(mode, lhs, setup_command(command), opts) + elseif not vim.tbl_contains(M._falsy_mapping_names, cmd) then + log.warn( + string.format("Invalid command in fuzzy_finder_mappings: ['%s'] = '%s'", lhs, cmd) + ) + end + end + elseif cmdtype == "function" then + ---@cast cmd -neotree.FuzzyFinder.VerboseCommand + input:map(mode, lhs, setup_command(cmd), opts) + end + end + end +end + +---@param input NuiInput +---@param cmds neotree.FuzzyFinder.BuiltinCommands +---@param state neotree.State +---@param scroll_padding integer +function M.setup_mappings(input, cmds, state, scroll_padding) + local config = require("neo-tree").config + + local ff_mappings = config.filesystem.window.fuzzy_finder_mappings or {} + apply_simple_mappings(input, cmds, state, scroll_padding, "i", ff_mappings) + + for _, mappings_by_mode in ipairs(ff_mappings) do + for mode, mappings in pairs(mappings_by_mode) do + apply_simple_mappings(input, cmds, state, scroll_padding, mode, mappings) + end + end +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/help.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/help.lua new file mode 100644 index 0000000..90d973f --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/help.lua @@ -0,0 +1,172 @@ +local Popup = require("nui.popup") +local NuiLine = require("nui.line") +local utils = require("neo-tree.utils") +local popups = require("neo-tree.ui.popups") +local highlights = require("neo-tree.ui.highlights") +local M = {} + +---@param text string +---@param highlight string? +local add_text = function(text, highlight) + local line = NuiLine() + line:append(text, highlight) + return line +end + +---@param state neotree.State +---@param prefix_key string? +local get_sub_keys = function(state, prefix_key) + local keys = utils.get_keys(state.resolved_mappings, true) + if prefix_key then + local len = prefix_key:len() + local sub_keys = {} + for _, key in ipairs(keys) do + if #key > len and key:sub(1, len) == prefix_key then + table.insert(sub_keys, key) + end + end + return sub_keys + else + return keys + end +end + +---@param key string +---@param prefix string? +local function key_minus_prefix(key, prefix) + if prefix then + return key:sub(prefix:len() + 1) + else + return key + end +end + +---Shows a help screen for the mapped commands when will execute those commands +---when the corresponding key is pressed. +---@param state neotree.State state of the source. +---@param title string? if this is a sub-menu for a multi-key mapping, the title for the window. +---@param prefix_key string? if this is a sub-menu, the start of tehe multi-key mapping +M.show = function(state, title, prefix_key) + local tree_width = vim.api.nvim_win_get_width(state.winid) + local keys = get_sub_keys(state, prefix_key) + + local lines = { add_text("") } + lines[1] = add_text(" Press the corresponding key to execute the command.", "Comment") + lines[2] = add_text(" Press to cancel.", "Comment") + lines[3] = add_text("") + local header = NuiLine() + header:append(string.format(" %14s", "KEY(S)"), highlights.ROOT_NAME) + header:append(" ", highlights.DIM_TEXT) + header:append("COMMAND", highlights.ROOT_NAME) + lines[4] = header + local max_width = #lines[1]:content() + for _, key in ipairs(keys) do + ---@type neotree.State.ResolvedMapping + local value = state.resolved_mappings[key] + or { text = "", handler = function() end } + local nline = NuiLine() + nline:append(string.format(" %14s", key_minus_prefix(key, prefix_key)), highlights.FILTER_TERM) + nline:append(" -> ", highlights.DIM_TEXT) + nline:append(value.text, highlights.NORMAL) + local line = nline:content() + if #line > max_width then + max_width = #line + end + table.insert(lines, nline) + end + + local width = math.min(60, max_width + 1) + local col + if state.current_position == "right" then + col = vim.o.columns - tree_width - width - 1 + else + col = tree_width - 1 + end + + ---@type nui_popup_options + local options = { + position = { + row = 2, + col = col, + }, + size = { + width = width, + height = #keys + 5, + }, + enter = true, + focusable = true, + zindex = 50, + relative = "editor", + win_options = { + foldenable = false, -- Prevent folds from hiding lines + }, + } + + ---@return integer lines The number of screen lines that the popup should occupy at most + local popup_max_height = function() + -- statusline + local statusline_lines = 0 + local laststatus = vim.o.laststatus + if laststatus ~= 0 then + local windows = vim.api.nvim_tabpage_list_wins(0) + if (laststatus == 1 and #windows > 1) or laststatus > 1 then + statusline_lines = 1 + end + end + -- tabs + local tab_lines = 0 + local showtabline = vim.o.showtabline + if showtabline ~= 0 then + local tabs = vim.api.nvim_list_tabpages() + if (showtabline == 1 and #tabs > 1) or showtabline == 2 then + tab_lines = 1 + end + end + return vim.o.lines - vim.o.cmdheight - statusline_lines - tab_lines - 2 + end + local max_height = popup_max_height() + if options.size.height > max_height then + options.size.height = max_height + end + + title = title or "Neotree Help" + options = popups.popup_options(title, width, options) + local popup = Popup(options) + popup:mount() + + local event = require("nui.utils.autocmd").event + popup:on({ event.VimResized }, function() + popup:update_layout({ + size = { + height = math.min(options.size.height --[[@as integer]], popup_max_height()), + width = math.min(options.size.width --[[@as integer]], vim.o.columns - 2), + }, + }) + end) + popup:on({ event.BufLeave, event.BufDelete }, function() + popup:unmount() + end, { once = true }) + + popup:map("n", "", function() + popup:unmount() + end, { noremap = true }) + + for _, key in ipairs(keys) do + -- map everything except for + if string.match(key:lower(), "^", handler = function() end } + popup:map("n", key_minus_prefix(key, prefix_key), function() + popup:unmount() + vim.api.nvim_set_current_win(state.winid) + value.handler() + end) + end + end + + for i, line in ipairs(lines) do + line:render(popup.bufnr, -1, i) + end +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/hijack_cursor.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/hijack_cursor.lua new file mode 100644 index 0000000..6ade731 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/hijack_cursor.lua @@ -0,0 +1,49 @@ +local events = require("neo-tree.events") +local log = require("neo-tree.log") +local manager = require("neo-tree.sources.manager") + +local M = {} + +local hijack_cursor_handler = function() + if vim.o.filetype ~= "neo-tree" then + return + end + local success, source = pcall(vim.api.nvim_buf_get_var, 0, "neo_tree_source") + if not success then + log.debug("Cursor hijack failure: " .. vim.inspect(source)) + return + end + local winid = nil + local _, position = pcall(vim.api.nvim_buf_get_var, 0, "neo_tree_position") + if position == "current" then + winid = vim.api.nvim_get_current_win() + end + + local state = manager.get_state(source, nil, winid) + if not state or not state.tree then + return + end + local node = state.tree:get_node() + if not node then + return + end + log.debug("Cursor moved in tree window, hijacking cursor position") + local cursor = vim.api.nvim_win_get_cursor(0) + local row = cursor[1] + local current_line = vim.api.nvim_get_current_line() + local startIndex, _ = string.find(current_line, node.name, nil, true) + if startIndex then + vim.api.nvim_win_set_cursor(0, { row, startIndex - 1 }) + end +end + +--Enables cursor hijack behavior for all sources +M.setup = function() + events.subscribe({ + event = events.VIM_CURSOR_MOVED, + handler = hijack_cursor_handler, + id = "neo-tree-hijack-cursor", + }) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/node_expander.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/node_expander.lua new file mode 100644 index 0000000..af3433b --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/node_expander.lua @@ -0,0 +1,85 @@ +local log = require("neo-tree.log") +local utils = require("neo-tree.utils") + +local M = {} + +--- Recursively expand all loaded nodes under the given node +--- returns table with all discovered nodes that need to be loaded +---@param node table a node to expand +---@param state neotree.State current state of the source +---@return table discovered nodes that need to be loaded +local function expand_loaded(node, state, prefetcher) + local function rec(current_node, to_load) + if prefetcher.should_prefetch(current_node) then + log.trace("Node " .. current_node:get_id() .. "not loaded, saving for later") + table.insert(to_load, current_node) + else + if not current_node:is_expanded() then + current_node:expand() + state.explicitly_opened_nodes[current_node:get_id()] = true + end + local children = state.tree:get_nodes(current_node:get_id()) + log.debug("Expanding childrens of " .. current_node:get_id()) + for _, child in ipairs(children) do + if utils.is_expandable(child) then + rec(child, to_load) + else + log.trace("Child: " .. (child.name or "") .. " is not expandable, skipping") + end + end + end + end + + local to_load = {} + rec(node, to_load) + return to_load +end + +--- Recursively expands all nodes under the given node collecting all unloaded nodes +--- Then run prefetcher on all unloaded nodes. Finally, expand loded nodes. +--- async method +---@param node table a node to expand +---@param state neotree.State current state of the source +local function expand_and_load(node, state, prefetcher) + local to_load = expand_loaded(node, state, prefetcher) + for _, _node in ipairs(to_load) do + prefetcher.prefetch(state, _node) + -- no need to handle results as prefetch is recursive + expand_loaded(_node, state, prefetcher) + end +end + +--- Expands given node recursively loading all descendant nodes if needed +--- Nodes will be loaded using given prefetcher +--- async method +---@param state neotree.State current state of the source +---@param node table a node to expand +---@param prefetcher table? an object with two methods `prefetch(state, node)` and `should_prefetch(node) => boolean` +M.expand_directory_recursively = function(state, node, prefetcher) + log.debug("Expanding directory " .. node:get_id()) + prefetcher = prefetcher or M.default_prefetcher + if not utils.is_expandable(node) then + return + end + + state.explicitly_opened_nodes = state.explicitly_opened_nodes or {} + if prefetcher.should_prefetch(node) then + local id = node:get_id() + state.explicitly_opened_nodes[id] = true + prefetcher.prefetch(state, node) + expand_loaded(node, state, prefetcher) + else + expand_and_load(node, state, prefetcher) + end +end + +M.default_prefetcher = { + prefetch = function(state, node) + log.debug("Default expander prefetch does nothing") + end, + should_prefetch = function(node) + return false + end, +} + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/preview.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/preview.lua new file mode 100644 index 0000000..24d88dc --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/common/preview.lua @@ -0,0 +1,572 @@ +local utils = require("neo-tree.utils") +local highlights = require("neo-tree.ui.highlights") +local events = require("neo-tree.events") +local manager = require("neo-tree.sources.manager") +local log = require("neo-tree.log") +local renderer = require("neo-tree.ui.renderer") +local NuiPopup = require("nui.popup") + +---@class neotree.Preview.Config +---@field use_float boolean? +---@field use_image_nvim boolean? +---@field use_snacks_image boolean? + +---@class neotree.Preview.Event +---@field source string? +---@field event neotree.event.Handler + +---@class neotree.Preview +---@field config neotree.Preview.Config? +---@field active boolean Whether the preview is active. +---@field winid integer The id of the window being used to preview. +---@field is_neo_tree_window boolean Whether the preview window belongs to neo-tree. +---@field bufnr number The buffer that is currently in the preview window. +---@field start_pos integer[]? An array-like table specifying the (0-indexed) starting position of the previewed text. +---@field end_pos integer[]? An array-like table specifying the (0-indexed) ending position of the preview text. +---@field truth table A table containing information to be restored when the preview ends. +---@field events neotree.Preview.Event[] A list of events the preview is subscribed to. +local Preview = {} + +---@type neotree.Preview? +local instance = nil + +local neo_tree_preview_namespace = vim.api.nvim_create_namespace("neo_tree_preview") + +---@param state neotree.State +local function create_floating_preview_window(state) + local default_position = utils.resolve_config_option(state, "window.position", "left") + state.current_position = state.current_position or default_position + + local title = state.config.title or "Neo-tree Preview" + local winwidth = vim.api.nvim_win_get_width(state.winid) + local winheight = vim.api.nvim_win_get_height(state.winid) + local height = vim.o.lines - 4 + local width = 120 + local row, col = 0, 0 + + if state.current_position == "left" then + col = winwidth + 1 + width = math.min(vim.o.columns - col, 120) + elseif state.current_position == "top" or state.current_position == "bottom" then + height = height - winheight + width = winwidth - 2 + if state.current_position == "top" then + row = vim.api.nvim_win_get_height(state.winid) + 1 + end + elseif state.current_position == "right" then + width = math.min(vim.o.columns - winwidth - 4, 120) + col = vim.o.columns - winwidth - width - 3 + elseif state.current_position == "float" then + local pos = vim.api.nvim_win_get_position(state.winid) + -- preview will be same height and top as tree + row = pos[1] + height = winheight + + -- tree and preview window will be side by side and centered in the editor + width = math.min(vim.o.columns - winwidth - 4, 120) + local total_width = winwidth + width + 4 + local margin = math.floor((vim.o.columns - total_width) / 2) + col = margin + winwidth + 2 + + -- move the tree window to make the combined layout centered + local popup = renderer.get_nui_popup(state.winid) + popup:update_layout({ + relative = "editor", + position = { + row = row, + col = margin, + }, + }) + else + local cur_pos = state.current_position or "unknown" + log.error('Preview cannot be used when position = "' .. cur_pos .. '"') + return + end + + if height < 5 or width < 5 then + log.error( + "Preview cannot be used without any space, please resize the neo-tree split to allow for at least 5 cells of free space." + ) + return + end + local popups = require("neo-tree.ui.popups") + local options = popups.popup_options(title, width, { + ns_id = highlights.ns_id, + size = { height = height, width = width }, + relative = "editor", + position = { + row = row, + col = col, + }, + win_options = { + number = true, + winhighlight = "Normal:" + .. highlights.FLOAT_NORMAL + .. ",FloatBorder:" + .. highlights.FLOAT_BORDER, + }, + }) + options.zindex = 40 + options.buf_options.filetype = "neo-tree-preview" + + local win = NuiPopup(options) + win:mount() + return win +end + +---Creates a new preview. +---@param state neotree.State The state of the source. +---@return neotree.Preview preview A new preview. A preview is a table consisting of the following keys: +--These keys should not be altered directly. Note that the keys `start_pos`, `end_pos` and `truth` +--may be inaccurate if `active` is false. +function Preview:new(state) + local preview = {} + preview.active = false + preview.config = vim.deepcopy(state.config) + setmetatable(preview, { __index = self }) + preview:findWindow(state) + return preview +end + +---Preview a buffer in the preview window and optionally reveal and highlight the previewed text. +---@param bufnr integer? The number of the buffer to be previewed. +---@param start_pos integer[]? The (0-indexed) starting position of the previewed text. May be absent. +---@param end_pos integer[]? The (0-indexed) ending position of the previewed text. May be absent +function Preview:preview(bufnr, start_pos, end_pos) + if self.is_neo_tree_window then + log.warn("Could not find appropriate window for preview") + return + end + + bufnr = bufnr or self.bufnr + if not self.active then + self:activate() + end + + if not self.active then + return + end + + self:setBuffer(bufnr) + + self.start_pos = start_pos + self.end_pos = end_pos + + self:reveal() + self:highlight_preview_range() +end + +---Reverts the preview and inactivates it, restoring the preview window to its previous state. +function Preview:revert() + self.active = false + self:unsubscribe() + + if not renderer.is_window_valid(self.winid) then + self.winid = nil + return + end + + if self.config.use_float then + vim.api.nvim_win_close(self.winid, true) + self.winid = nil + return + else + local foldenable = utils.get_value(self.truth, "options.foldenable", nil, false) + if foldenable ~= nil then + vim.wo[self.winid].foldenable = self.truth.options.foldenable + end + vim.api.nvim_win_set_var(self.winid, "neo_tree_preview", 0) + end + + local bufnr = self.truth.bufnr + if type(bufnr) ~= "number" then + return + end + if not vim.api.nvim_buf_is_valid(bufnr) then + return + end + self:setBuffer(bufnr) + if vim.api.nvim_win_is_valid(self.winid) then + vim.api.nvim_win_call(self.winid, function() + vim.fn.winrestview(self.truth.view) + end) + end + vim.bo[self.bufnr].bufhidden = self.truth.options.bufhidden +end + +---Subscribe to event and add it to the preview event list. +---@param source string? Name of the source to add the event to. Will use `events.subscribe` if nil. +---@param event neotree.event.Handler Event to subscribe to. +function Preview:subscribe(source, event) + if source == nil then + events.subscribe(event) + else + manager.subscribe(source, event) + end + self.events = self.events or {} + table.insert(self.events, { source = source, event = event }) +end + +---Unsubscribe to all events in the preview event list. +function Preview:unsubscribe() + if self.events == nil then + return + end + for _, event in ipairs(self.events) do + if event.source == nil then + events.unsubscribe(event.event) + else + manager.unsubscribe(event.source, event.event) + end + end + self.events = {} +end + +---Finds the appropriate window and updates the preview accordingly. +---@param state neotree.State The state of the source. +function Preview:findWindow(state) + local winid, is_neo_tree_window + if self.config.use_float then + if + type(self.winid) == "number" + and vim.api.nvim_win_is_valid(self.winid) + and utils.is_floating(self.winid) + then + return + end + local win = create_floating_preview_window(state) + if not win then + self.active = false + return + end + winid = win.winid + is_neo_tree_window = false + else + winid, is_neo_tree_window = utils.get_appropriate_window(state) + self.bufnr = vim.api.nvim_win_get_buf(winid) + end + + if winid == self.winid then + return + end + self.winid, self.is_neo_tree_window = winid, is_neo_tree_window + + if self.active then + self:revert() + self:preview() + end +end + +---Activates the preview, but does not populate the preview window, +function Preview:activate() + if self.active then + return + end + if not renderer.is_window_valid(self.winid) then + return + end + if self.config.use_float then + self.bufnr = vim.api.nvim_create_buf(false, true) + self.truth = {} + else + self.truth = { + bufnr = self.bufnr, + view = vim.api.nvim_win_call(self.winid, vim.fn.winsaveview), + options = { + bufhidden = vim.bo[self.bufnr].bufhidden, + foldenable = vim.wo[self.winid].foldenable, + }, + } + vim.bo[self.bufnr].bufhidden = "hide" + vim.wo[self.winid].foldenable = false + end + self.active = true + vim.api.nvim_win_set_var(self.winid, "neo_tree_preview", 1) +end + +---@param winid number +---@param bufnr number +---@return boolean hijacked Whether the buffer was successfully hijacked. +local function try_load_image_nvim_buf(winid, bufnr) + -- notify only image.nvim to let it try and hijack + local image_augroup = vim.api.nvim_create_augroup("image.nvim", { clear = false }) + if #vim.api.nvim_get_autocmds({ group = image_augroup }) == 0 then + local image_available, image = pcall(require, "image") + if not image_available then + local image_nvim_url = "https://github.com/3rd/image.nvim" + log.debug( + "use_image_nvim was set but image.nvim was not found. Install from: " .. image_nvim_url + ) + return false + end + log.warn("image.nvim was not setup. Calling require('image').setup().") + image.setup() + end + + vim.opt.eventignore:remove("BufWinEnter") + local ok = pcall(vim.api.nvim_win_call, winid, function() + vim.api.nvim_exec_autocmds("BufWinEnter", { group = image_augroup, buffer = bufnr }) + end) + vim.opt.eventignore:append("BufWinEnter") + if not ok then + log.debug("image.nvim doesn't have any file patterns to hijack.") + return false + end + if vim.bo[bufnr].filetype ~= "image_nvim" then + return false + end + return true +end + +---@param bufnr number The buffer number of the buffer to set. +---@return number bytecount The number of bytes in the buffer +local get_bufsize = function(bufnr) + return vim.api.nvim_buf_call(bufnr, function() + return vim.fn.line2byte(vim.fn.line("$") + 1) + end) +end + +events.subscribe({ + event = events.NEO_TREE_PREVIEW_BEFORE_RENDER, + ---@param args neotree.event.args.PREVIEW_BEFORE_RENDER + handler = function(args) + local preview = args.preview + local bufnr = args.bufnr + + if not preview.config.use_snacks_image then + return + end + -- check if snacks.image is available + local snacks_image_ok, image = pcall(require, "snacks.image") + if not snacks_image_ok then + local snacks_nvim_url = "https://github.com/folke/snacks.nvim" + log.debug( + "use_snacks_image was set but snacks.nvim was not found. Install from: " .. snacks_nvim_url + ) + return + end + local bufname = vim.api.nvim_buf_get_name(bufnr) + -- try attaching it + if image.supports(bufname) then + image.placement.new(preview.bufnr, bufname) + vim.bo[preview.bufnr].modifiable = true + return { handled = true } -- let snacks.image handle the rest + end + end, +}) +events.subscribe({ + event = events.NEO_TREE_PREVIEW_BEFORE_RENDER, + ---@param args neotree.event.args.PREVIEW_BEFORE_RENDER + handler = function(args) + local preview = args.preview + local bufnr = args.bufnr + + if preview.config.use_image_nvim and try_load_image_nvim_buf(preview.winid, bufnr) then + -- calling the try method twice should be okay here, image.nvim should cache the image and displaying the image takes + -- really long anyways + vim.api.nvim_win_set_buf(preview.winid, bufnr) + return { handled = try_load_image_nvim_buf(preview.winid, bufnr) } + end + end, +}) + +---Set the buffer in the preview window without executing BufEnter or BufWinEnter autocommands. +---@param bufnr number The buffer number of the buffer to set. +function Preview:setBuffer(bufnr) + self:clearHighlight() + if bufnr == self.bufnr then + return + end + local eventignore = vim.opt.eventignore + vim.opt.eventignore:append("BufEnter,BufWinEnter") + + repeat + ---@class neotree.event.args.PREVIEW_BEFORE_RENDER + local args = { + preview = self, + bufnr = bufnr, + } + events.fire_event(events.NEO_TREE_PREVIEW_BEFORE_RENDER, args) + + if self.config.use_float then + -- Workaround until https://github.com/neovim/neovim/issues/24973 is resolved or maybe 'previewpopup' comes in? + vim.fn.bufload(bufnr) + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, lines) + vim.api.nvim_win_set_buf(self.winid, self.bufnr) + -- I'm not sure why float windows won't show numbers without this + vim.wo[self.winid].number = true + + -- code below is from mini.pick + -- only starts treesitter parser if the filetype is matching + local ft = vim.bo[bufnr].filetype + local bufsize = get_bufsize(bufnr) + if bufsize > 1024 * 1024 or bufsize > 1000 * #lines then + break -- goto end + end + local has_lang, lang = pcall(vim.treesitter.language.get_lang, ft) + lang = has_lang and lang or ft + local has_parser, parser = + pcall(vim.treesitter.get_parser, self.bufnr, lang, { error = false }) + has_parser = has_parser and parser ~= nil + if has_parser then + has_parser = pcall(vim.treesitter.start, self.bufnr, lang) + end + if not has_parser then + vim.bo[self.bufnr].syntax = ft + end + else + vim.api.nvim_win_set_buf(self.winid, bufnr) + self.bufnr = bufnr + end + + until true + vim.opt.eventignore = eventignore +end + +---Move the cursor to the previewed position and center the screen. +function Preview:reveal() + local pos = self.start_pos or self.end_pos + if not self.active or not self.winid or not pos then + return + end + vim.api.nvim_win_set_cursor(self.winid, { (pos[1] or 0) + 1, pos[2] or 0 }) + vim.api.nvim_win_call(self.winid, function() + vim.cmd("normal! zz") + end) +end + +---Highlight the previewed range +function Preview:highlight_preview_range() + if not self.active or not self.bufnr then + return + end + local start_pos, end_pos = self.start_pos, self.end_pos + if not start_pos and not end_pos then + return + end + + if not start_pos then + ---@cast end_pos table + start_pos = end_pos + elseif not end_pos then + ---@cast start_pos table + end_pos = start_pos + end + + local start_line, end_line = start_pos[1], end_pos[1] + local start_col, end_col = start_pos[2], end_pos[2] + vim.api.nvim_buf_set_extmark(self.bufnr, neo_tree_preview_namespace, start_line, start_col, { + hl_group = highlights.PREVIEW, + end_row = end_line, + end_col = end_col, + -- priority = priority, + strict = false, + }) +end + +---Clear the preview highlight in the buffer currently in the preview window. +function Preview:clearHighlight() + if type(self.bufnr) == "number" and vim.api.nvim_buf_is_valid(self.bufnr) then + vim.api.nvim_buf_clear_namespace(self.bufnr, neo_tree_preview_namespace, 0, -1) + end +end + +local toggle_state = false + +Preview.hide = function() + toggle_state = false + if instance then + instance:revert() + end + instance = nil +end + +Preview.is_active = function() + return instance and instance.active +end + +---@param state neotree.State +Preview.show = function(state) + local node = assert(state.tree:get_node()) + + if instance then + instance:findWindow(state) + else + instance = Preview:new(state) + end + + local extra = node.extra or {} + local position = extra.position + local end_position = extra.end_position + local path = node.path or node:get_id() + local bufnr = extra.bufnr or vim.fn.bufadd(path) + + if bufnr and bufnr > 0 and instance then + instance:preview(bufnr, position, end_position) + end +end + +---@param state neotree.State +Preview.toggle = function(state) + if toggle_state then + Preview.hide() + else + Preview.show(state) + if instance and instance.active then + toggle_state = true + else + Preview.hide() + return + end + local winid = state.winid + local source_name = state.name + local preview_event = { + event = events.VIM_CURSOR_MOVED, + handler = function() + local did_enter_preview = vim.api.nvim_get_current_win() == instance.winid + if not toggle_state or (did_enter_preview and instance.config.use_float) then + return + end + if vim.api.nvim_get_current_win() == winid then + log.debug("Cursor moved in tree window, updating preview") + Preview.show(state) + else + log.debug("Neo-tree window lost focus, disposing preview") + Preview.hide() + end + end, + id = "preview-event", + } + instance:subscribe(source_name, preview_event) + end +end + +Preview.focus = function() + if Preview.is_active() then + ---@cast instance table + vim.fn.win_gotoid(instance.winid) + end +end + +local CTRL_E = utils.keycode("") +local CTRL_Y = utils.keycode("") +---@param state neotree.State +Preview.scroll = function(state) + local direction = state.config.direction + local input = direction < 0 and CTRL_E or CTRL_Y + local count = math.abs(direction) + + if Preview:is_active() then + ---@cast instance table + vim.api.nvim_win_call(instance.winid, function() + vim.cmd(("normal! %s%s"):format(count, input)) + end) + else + vim.api.nvim_win_call(state.winid, function() + vim.api.nvim_feedkeys(state.fallback, "n", false) + end) + end +end + +return Preview diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/commands.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/commands.lua new file mode 100644 index 0000000..3c1b1cc --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/commands.lua @@ -0,0 +1,71 @@ +--This file should contain all commands meant to be used by mappings. +local cc = require("neo-tree.sources.common.commands") +local utils = require("neo-tree.utils") +local manager = require("neo-tree.sources.manager") +local inputs = require("neo-tree.ui.inputs") +local filters = require("neo-tree.sources.common.filters") + +---@class neotree.sources.DocumentSymbols.Commands : neotree.sources.Common.Commands +---@field [string] neotree.TreeCommand +local M = {} +local SOURCE_NAME = "document_symbols" +M.refresh = utils.wrap(manager.refresh, SOURCE_NAME) +M.redraw = utils.wrap(manager.redraw, SOURCE_NAME) + +M.show_debug_info = function(state) + print(vim.inspect(state)) +end + +---@param node NuiTree.Node +M.jump_to_symbol = function(state, node) + node = node or state.tree:get_node() + if node:get_depth() == 1 then + return + end + vim.api.nvim_set_current_win(state.lsp_winid) + vim.api.nvim_set_current_buf(state.lsp_bufnr) + local symbol_loc = node.extra.selection_range.start + vim.api.nvim_win_set_cursor(state.lsp_winid, { symbol_loc[1] + 1, symbol_loc[2] }) +end + +M.rename = function(state) + local node = assert(state.tree:get_node()) + if node:get_depth() == 1 then + return + end + local old_name = node.name + + ---@param new_name string? + local callback = function(new_name) + if not new_name or new_name == "" or new_name == old_name then + return + end + M.jump_to_symbol(state, node) + vim.lsp.buf.rename(new_name) + M.refresh(state) + end + local msg = string.format('Enter new name for "%s":', old_name) + inputs.input(msg, old_name, callback) +end + +M.open = M.jump_to_symbol + +M.filter_on_submit = function(state) + filters.show_filter(state, true, true) +end + +M.filter = function(state) + filters.show_filter(state, true) +end + +cc._add_common_commands(M, "node") -- common tree commands +cc._add_common_commands(M, "^open") -- open commands +cc._add_common_commands(M, "^close_window$") +cc._add_common_commands(M, "source$") -- source navigation +cc._add_common_commands(M, "preview") -- preview +cc._add_common_commands(M, "^cancel$") -- cancel +cc._add_common_commands(M, "help") -- help commands +cc._add_common_commands(M, "with_window_picker$") -- open using window picker +cc._add_common_commands(M, "^toggle_auto_expand_width$") + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/components.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/components.lua new file mode 100644 index 0000000..fed89da --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/components.lua @@ -0,0 +1,66 @@ +-- This file contains the built-in components. Each componment is a function +-- that takes the following arguments: +-- config: A table containing the configuration provided by the user +-- when declaring this component in their renderer config. +-- node: A NuiNode object for the currently focused node. +-- state: The current state of the source providing the items. +-- +-- The function should return either a table, or a list of tables, each of which +-- contains the following keys: +-- text: The text to display for this item. +-- highlight: The highlight group to apply to this text. + +local highlights = require("neo-tree.ui.highlights") +local common = require("neo-tree.sources.common.components") + +---@alias neotree.Component.DocumentSymbols._Key +---|"kind_icon" +---|"kind_name" +---|"name" + +---@class neotree.Component.DocumentSymbols Use the neotree.Component.DocumentSymbols.* types to get more specific types. +---@field [1] neotree.Component.DocumentSymbols._Key|neotree.Component.Common._Key + +---@type table +local M = {} + +---@class (exact) neotree.Component.DocumentSymbols.KindIcon : neotree.Component +---@field [1] "kind_icon"? +---@field provider neotree.IconProvider? + +---@param config neotree.Component.DocumentSymbols.KindIcon +M.kind_icon = function(config, node, state) + local icon = { + text = node:get_depth() == 1 and "" or node.extra.kind.icon, + highlight = node.extra.kind.hl, + } + + if config.provider then + icon = config.provider(icon, node, state) or icon + end + + return icon +end + +---@class (exact) neotree.Component.DocumentSymbols.KindName : neotree.Component +---@field [1] "kind_name"? + +---@param config neotree.Component.DocumentSymbols.KindName +M.kind_name = function(config, node, state) + return { + text = node:get_depth() == 1 and "" or node.extra.kind.name, + highlight = node.extra and node.extra.kind.hl or highlights.FILE_NAME, + } +end + +---@class (exact) neotree.Component.DocumentSymbols.Name : neotree.Component.Common.Name + +---@param config neotree.Component.DocumentSymbols.Name +M.name = function(config, node, state) + return { + text = node.name, + highlight = node.extra and node.extra.kind.hl or highlights.FILE_NAME, + } +end + +return vim.tbl_deep_extend("force", common, M) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/init.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/init.lua new file mode 100644 index 0000000..449cf47 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/init.lua @@ -0,0 +1,129 @@ +--This file should have all functions that are in the public api and either set +--or read the state of this source. + +local manager = require("neo-tree.sources.manager") +local events = require("neo-tree.events") +local utils = require("neo-tree.utils") +local symbols = require("neo-tree.sources.document_symbols.lib.symbols_utils") +local renderer = require("neo-tree.ui.renderer") + +---@class neotree.sources.DocumentSymbols : neotree.Source +local M = { + name = "document_symbols", + display_name = "  Symbols ", +} + +local get_state = function() + return manager.get_state(M.name) +end + +---Refresh the source with debouncing +---@param args { afile: string } +local refresh_debounced = function(args) + if utils.is_real_file(args.afile) == false then + return + end + utils.debounce( + "document_symbols_refresh", + utils.wrap(manager.refresh, M.name), + 100, + utils.debounce_strategy.CALL_LAST_ONLY + ) +end + +---Internal function to follow the cursor +local follow_symbol = function() + local state = get_state() + if state.lsp_bufnr ~= vim.api.nvim_get_current_buf() then + return + end + local cursor = vim.api.nvim_win_get_cursor(state.lsp_winid) + local node_id = symbols.get_symbol_by_loc(state.tree, { cursor[1] - 1, cursor[2] }) + if #node_id > 0 then + renderer.focus_node(state, node_id, true) + end +end + +---@class neotree.sources.documentsymbols.DebounceArgs + +---Follow the cursor with debouncing +---@param args { afile: string } +local follow_debounced = function(args) + if utils.is_real_file(args.afile) == false then + return + end + utils.debounce( + "document_symbols_follow", + utils.wrap(follow_symbol, args.afile), + 100, + utils.debounce_strategy.CALL_LAST_ONLY + ) +end + +---Navigate to the given path. +M.navigate = function(state, path, path_to_reveal, callback, async) + state.lsp_winid, _ = utils.get_appropriate_window(state) + state.lsp_bufnr = vim.api.nvim_win_get_buf(state.lsp_winid) + state.path = vim.api.nvim_buf_get_name(state.lsp_bufnr) + + symbols.render_symbols(state) + + if type(callback) == "function" then + vim.schedule(callback) + end +end + +---@class neotree.Config.LspKindDisplay +---@field icon string +---@field hl string + +---@class neotree.Config.DocumentSymbols.Renderers : neotree.Config.Renderers +---@field root neotree.Component.DocumentSymbols[]? +---@field symbol neotree.Component.DocumentSymbols[]? + +---@class (exact) neotree.Config.DocumentSymbols : neotree.Config.Source +---@field follow_cursor boolean? +---@field client_filters neotree.lsp.ClientFilter? +---@field custom_kinds table? +---@field kinds table? +---@field renderers neotree.Config.DocumentSymbols.Renderers? + +---Configures the plugin, should be called before the plugin is used. +---@param config neotree.Config.DocumentSymbols +---@param global_config neotree.Config.Base +M.setup = function(config, global_config) + symbols.setup(config) + + if config.before_render then + manager.subscribe(M.name, { + event = events.BEFORE_RENDER, + handler = function(state) + local this_state = get_state() + if state == this_state then + config.before_render(this_state) + end + end, + }) + end + + local refresh_events = { + events.VIM_BUFFER_ENTER, + events.VIM_INSERT_LEAVE, + events.VIM_TEXT_CHANGED_NORMAL, + } + for _, event in ipairs(refresh_events) do + manager.subscribe(M.name, { + event = event, + handler = refresh_debounced, + }) + end + + if config.follow_cursor then + manager.subscribe(M.name, { + event = events.VIM_CURSOR_MOVED, + handler = follow_debounced, + }) + end +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/lib/client_filters.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/lib/client_filters.lua new file mode 100644 index 0000000..6e9cfea --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/lib/client_filters.lua @@ -0,0 +1,97 @@ +---Utilities function to filter the LSP servers +local utils = require("neo-tree.utils") + +---@class neotree.lsp.RespRaw +---@field err lsp.ResponseError? +---@field error lsp.ResponseError? +---@field result any + +local M = {} + +---@alias neotree.lsp.Filter fun(client_name: string): boolean + +---Filter clients +---@param filter_type "first" | "all" +---@param filter_fn neotree.lsp.Filter? +---@param resp table +---@return table +local filter_clients = function(filter_type, filter_fn, resp) + if resp == nil or type(resp) ~= "table" then + return {} + end + filter_fn = filter_fn or function(client_name) + return true + end + + local result = {} + for client_id, client_resp in pairs(resp) do + local client_name = vim.lsp.get_client_by_id(client_id).name + if filter_fn(client_name) and client_resp.result ~= nil then + result[client_name] = client_resp.result + if filter_type ~= "all" then + break + end + end + end + return result +end + +---Filter only allowed clients +---@param allow_only string[] the list of clients to keep +---@return neotree.lsp.Filter +local allow_only = function(allow_only) + return function(client_name) + return vim.tbl_contains(allow_only, client_name) + end +end + +---Ignore clients +---@param ignore string[] the list of clients to remove +---@return neotree.lsp.Filter +local ignore = function(ignore) + return function(client_name) + return not vim.tbl_contains(ignore, client_name) + end +end + +---Main entry point for the filter +---@param resp table +---@return table +M.filter_resp = function(resp) + return {} +end + +---@alias neotree.lsp.Filter.Type +---|"first" # Allow the first that matches +---|"all" # Allow all that match + +---@alias neotree.lsp.ClientFilter neotree.lsp.Filter.Type | { type: neotree.lsp.Filter.Type, fn: neotree.lsp.Filter, allow_only: string[], ignore: string[] } +---Setup the filter accordingly to the config +---@see neo-tree-document-symbols-source for more details on options that the filter accepts +---@param cfg_flt neotree.lsp.ClientFilter +M.setup = function(cfg_flt) + local filter_type = "first" + local filter_fn = nil + + if type(cfg_flt) == "table" then + if cfg_flt.type == "all" then + filter_type = "all" + end + + if cfg_flt.fn ~= nil then + filter_fn = cfg_flt.fn + elseif cfg_flt.allow_only then + filter_fn = allow_only(cfg_flt.allow_only) + elseif cfg_flt.ignore then + filter_fn = ignore(cfg_flt.ignore) + end + elseif cfg_flt == "all" then + filter_type = "all" + end + + M.filter_resp = function(resp) + return filter_clients(filter_type, filter_fn, resp) + end +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/lib/kinds.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/lib/kinds.lua new file mode 100644 index 0000000..cc13bbc --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/lib/kinds.lua @@ -0,0 +1,67 @@ +---Helper module to render symbols' kinds +---Need to be initialized by calling M.setup() +local M = {} + +local kinds_id_to_name = { + [0] = "Root", + [1] = "File", + [2] = "Module", + [3] = "Namespace", + [4] = "Package", + [5] = "Class", + [6] = "Method", + [7] = "Property", + [8] = "Field", + [9] = "Constructor", + [10] = "Enum", + [11] = "Interface", + [12] = "Function", + [13] = "Variable", + [14] = "Constant", + [15] = "String", + [16] = "Number", + [17] = "Boolean", + [18] = "Array", + [19] = "Object", + [20] = "Key", + [21] = "Null", + [22] = "EnumMember", + [23] = "Struct", + [24] = "Event", + [25] = "Operator", + [26] = "TypeParameter", +} + +local kinds_map = {} + +---@class neotree.LspKindDisplay +---@field name string Display name +---@field icon string Icon to render +---@field hl string Highlight for the node + +---Get how the kind with kind_id should be rendered +---@param kind_id integer the kind_id to be render +---@return neotree.LspKindDisplay res +M.get_kind = function(kind_id) + local kind_name = kinds_id_to_name[kind_id] + return vim.tbl_extend( + "force", + { name = kind_name or ("Unknown: " .. kind_id), icon = "?", hl = "" }, + kind_name and (kinds_map[kind_name] or {}) or kinds_map["Unknown"] + ) +end + +---Setup the module with custom kinds +---@param custom_kinds table additional kinds, should be of the form { [kind_id] = kind_name } +---@param kinds_display table mapping of kind_name to corresponding display name, icon and hl group +--- { [kind_name] = { +--- name = kind_display_name, +--- icon = kind_icon, +--- hl = kind_hl +--- }, } +M.setup = function(custom_kinds, kinds_display) + kinds_id_to_name = vim.tbl_deep_extend("force", kinds_id_to_name, custom_kinds or {}) + kinds_map = kinds_display +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/lib/symbols_utils.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/lib/symbols_utils.lua new file mode 100644 index 0000000..29f6814 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/document_symbols/lib/symbols_utils.lua @@ -0,0 +1,211 @@ +---Utilities functions for the document_symbols source +local renderer = require("neo-tree.ui.renderer") +local filters = require("neo-tree.sources.document_symbols.lib.client_filters") +local kinds = require("neo-tree.sources.document_symbols.lib.kinds") + +local M = {} + +---@alias Loc integer[] a location in a buffer {row, col}, 0-indexed +---@alias LocRange { start: Loc, ["end"]: Loc } a range consisting of two loc + +---@class neotree.SymbolExtra +---@field bufnr integer the buffer containing the symbols, +---@field kind neotree.LspKindDisplay the kind of each symbol +---@field selection_range LocRange the symbol's location +---@field position Loc start of symbol's definition +---@field end_position Loc start of symbol's definition + +---@class neotree.SymbolNode see +---@field id string +---@field name string name of symbol +---@field path string buffer path - should all be the same +---@field type "root"|"symbol" +---@field children neotree.SymbolNode[] +---@field extra neotree.SymbolExtra additional info + +---Parse the lsp.Range +---@param range lsp.Range the lsp.Range object to parse +---@return LocRange range the parsed range +local parse_range = function(range) + return { + start = { range.start.line, range.start.character }, + ["end"] = { range["end"].line, range["end"].character }, + } +end + +---Compare two tuples of length 2 by first - second elements +---@param a Loc +---@param b Loc +---@return boolean +local loc_less_than = function(a, b) + if a[1] < b[1] then + return true + elseif a[1] == b[1] then + return a[2] <= b[2] + end + return false +end + +---Check whether loc is contained in range, i.e range[1] <= loc <= range[2] +---@param loc Loc +---@param range LocRange +---@return boolean +M.is_loc_in_range = function(loc, range) + return loc_less_than(range[1], loc) and loc_less_than(loc, range[2]) +end + +---Get the the current symbol under the cursor +---@param tree any the Nui symbol tree +---@param loc Loc the cursor location {row, col} (0-index) +---@return string node_id +M.get_symbol_by_loc = function(tree, loc) + local function dfs(node) + local node_id = node:get_id() + if node:has_children() then + for _, child in ipairs(tree:get_nodes(node_id)) do + if M.is_loc_in_range(loc, { child.extra.position, child.extra.end_position }) then + return dfs(child) + end + end + end + return node_id + end + + for _, root in ipairs(tree:get_nodes()) do + local node_id = dfs(root) + if node_id ~= root:get_id() then + return node_id + end + end + return "" +end + +---Parse the LSP response into a tree. Each node on the tree follows +---the same structure as a NuiTree node, with the extra field +---containing additional information. +---@param resp_node lsp.DocumentSymbol|lsp.SymbolInformation the LSP response node +---@param id string the id of the current node +---@return neotree.SymbolNode symb_node the parsed tree +local function parse_resp(resp_node, id, state, parent_search_path) + -- parse all children + local children = {} + local search_path = parent_search_path .. "/" .. resp_node.name + for i, child in ipairs(resp_node.children or {}) do + local child_node = parse_resp(child, id .. "." .. i, state, search_path) + table.insert(children, child_node) + end + + ---@type neotree.SymbolNode + local symbol_node = { + id = id, + name = resp_node.name, + type = "symbol", + path = state.path, + children = children, + ---@diagnostic disable-next-line: missing-fields + extra = { + bufnr = state.lsp_bufnr, + kind = kinds.get_kind(resp_node.kind), + search_path = search_path, + -- detail = resp_node.detail, + }, + } + local preview_range = resp_node.range + if preview_range then + ---@cast resp_node lsp.DocumentSymbol + symbol_node.extra.selection_range = parse_range(resp_node.selectionRange) + else + ---@cast resp_node lsp.SymbolInformation + preview_range = resp_node.location.range + symbol_node.extra.selection_range = parse_range(preview_range) + end + + preview_range = parse_range(preview_range) + symbol_node.extra.position = preview_range.start + symbol_node.extra.end_position = preview_range["end"] + return symbol_node +end + +---Callback function for lsp request +---@param lsp_resp table the response of the lsp clients +---@param state neotree.State the state of the source +local on_lsp_resp = function(lsp_resp, state) + if lsp_resp == nil or type(lsp_resp) ~= "table" then + return + end + + -- filter the response to get only the desired LSP + local resp = filters.filter_resp(lsp_resp) + + local bufname = assert(state.path) + local items = {} + + -- parse each client's response + for client_name, client_result in pairs(resp) do + local symbol_list = {} + for i, resp_node in ipairs(client_result) do + table.insert(symbol_list, parse_resp(resp_node, #items .. "." .. i, state, "/")) + end + + -- add the parsed response to the tree + local splits = vim.split(bufname, "/") + local filename = splits[#splits] + table.insert(items, { + id = "" .. #items, + name = string.format("SYMBOLS (%s) in %s", client_name, filename), + path = bufname, + type = "root", + children = symbol_list, + extra = { kind = kinds.get_kind(0), search_path = "/" }, + }) + end + renderer.show_nodes(items, state) +end + +---latter is deprecated in neovim v0.11 +---@diagnostic disable-next-line: deprecated +local get_clients = vim.lsp.get_clients or vim.lsp.get_active_clients +M.render_symbols = function(state) + local bufnr = state.lsp_bufnr + local bufname = state.path + + -- if no client found, terminate + local client_found = false + for _, client in pairs(get_clients({ bufnr = bufnr })) do + if client.server_capabilities.documentSymbolProvider then + client_found = true + break + end + end + if not client_found then + local splits = vim.split(bufname, "/") + renderer.show_nodes({ + { + id = "0", + name = "No client found for " .. splits[#splits], + path = bufname, + type = "root", + children = {}, + extra = { kind = kinds.get_kind(0), search_path = "/" }, + }, + }, state) + return + end + + -- client found + vim.lsp.buf_request_all( + bufnr, + "textDocument/documentSymbol", + { textDocument = vim.lsp.util.make_text_document_params(bufnr) }, + function(resp) + on_lsp_resp(resp, state) + end + ) +end + +M.setup = function(config) + filters.setup(config.client_filters) + kinds.setup(config.custom_kinds, config.kinds) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/commands.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/commands.lua new file mode 100644 index 0000000..1dcfd1f --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/commands.lua @@ -0,0 +1,284 @@ +--This file should contain all commands meant to be used by mappings. + +local cc = require("neo-tree.sources.common.commands") +local fs = require("neo-tree.sources.filesystem") +local utils = require("neo-tree.utils") +local filter = require("neo-tree.sources.filesystem.lib.filter") +local renderer = require("neo-tree.ui.renderer") +local log = require("neo-tree.log") +local uv = vim.uv or vim.loop + +---@class neotree.sources.Filesystem.Commands : neotree.sources.Common.Commands +local M = {} +local refresh = function(state) + fs._navigate_internal(state, nil, nil, nil, false) +end + +local redraw = function(state) + renderer.redraw(state) +end + +M.add = function(state) + cc.add(state, utils.wrap(fs.show_new_children, state)) +end + +M.add_directory = function(state) + cc.add_directory(state, utils.wrap(fs.show_new_children, state)) +end + +M.clear_filter = function(state) + fs.reset_search(state, true) +end + +M.copy = function(state) + cc.copy(state, utils.wrap(fs.focus_destination_children, state)) +end + +---Marks node as copied, so that it can be pasted somewhere else. +M.copy_to_clipboard = function(state) + cc.copy_to_clipboard(state, utils.wrap(redraw, state)) +end + +---@type neotree.TreeCommandVisual +M.copy_to_clipboard_visual = function(state, selected_nodes) + cc.copy_to_clipboard_visual(state, selected_nodes, utils.wrap(redraw, state)) +end + +---Marks node as cut, so that it can be pasted (moved) somewhere else. +M.cut_to_clipboard = function(state) + cc.cut_to_clipboard(state, utils.wrap(redraw, state)) +end + +---@type neotree.TreeCommandVisual +M.cut_to_clipboard_visual = function(state, selected_nodes) + cc.cut_to_clipboard_visual(state, selected_nodes, utils.wrap(redraw, state)) +end + +M.move = function(state) + cc.move(state, utils.wrap(fs.focus_destination_children, state)) +end + +---Pastes all items from the clipboard to the current directory. +M.paste_from_clipboard = function(state) + cc.paste_from_clipboard(state, utils.wrap(fs.show_new_children, state)) +end + +M.delete = function(state) + cc.delete(state, utils.wrap(refresh, state)) +end + +---@type neotree.TreeCommandVisual +M.delete_visual = function(state, selected_nodes) + cc.delete_visual(state, selected_nodes, utils.wrap(refresh, state)) +end + +M.expand_all_nodes = function(state, node) + cc.expand_all_nodes(state, node, fs.prefetcher) +end + +M.expand_all_subnodes = function(state, node) + cc.expand_all_subnodes(state, node, fs.prefetcher) +end + +---Shows the filter input, which will filter the tree. +---@param state neotree.sources.filesystem.State +M.filter_as_you_type = function(state) + local config = state.config or {} + filter.show_filter(state, true, false, false, config.keep_filter_on_submit or false) +end + +---Shows the filter input, which will filter the tree. +---@param state neotree.sources.filesystem.State +M.filter_on_submit = function(state) + filter.show_filter(state, false, false, false, true) +end + +---Shows the filter input in fuzzy finder mode. +---@param state neotree.sources.filesystem.State +M.fuzzy_finder = function(state) + local config = state.config or {} + filter.show_filter(state, true, true, false, config.keep_filter_on_submit or false) +end + +---Shows the filter input in fuzzy finder mode. +---@param state neotree.sources.filesystem.State +M.fuzzy_finder_directory = function(state) + local config = state.config or {} + filter.show_filter(state, true, "directory", false, config.keep_filter_on_submit or false) +end + +---Shows the filter input in fuzzy sorter +---@param state neotree.sources.filesystem.State +M.fuzzy_sorter = function(state) + local config = state.config or {} + filter.show_filter(state, true, true, true, config.keep_filter_on_submit or false) +end + +---Shows the filter input in fuzzy sorter with only directories +---@param state neotree.sources.filesystem.State +M.fuzzy_sorter_directory = function(state) + local config = state.config or {} + filter.show_filter(state, true, "directory", true, config.keep_filter_on_submit or false) +end + +---Navigate up one level. +---@param state neotree.sources.filesystem.State +M.navigate_up = function(state) + local parent_path, _ = utils.split_path(state.path) + if not utils.truthy(parent_path) then + return + end + local path_to_reveal = nil + local node = state.tree:get_node() + if node then + path_to_reveal = node:get_id() + end + if state.search_pattern then + fs.reset_search(state, false) + end + log.debug("Changing directory to:", parent_path) + fs._navigate_internal(state, parent_path, path_to_reveal, nil, false) +end + +local focus_next_git_modified = function(state, reverse) + local node = state.tree:get_node() + local current_path = node:get_id() + local g = state.git_status_lookup + if not utils.truthy(g) then + return + end + local paths = { current_path } + for path, status in pairs(g) do + if path ~= current_path and status and status ~= "!!" then + --don't include files not in the current working directory + if utils.is_subpath(state.path, path) then + table.insert(paths, path) + end + end + end + local sorted_paths = utils.sort_by_tree_display(paths) + if reverse then + sorted_paths = utils.reverse_list(sorted_paths) + end + + local is_file = function(path) + local success, stats = pcall(uv.fs_stat, path) + return (success and stats and stats.type ~= "directory") + end + + local passed = false + local target = nil + for _, path in ipairs(sorted_paths) do + if target == nil and is_file(path) then + target = path + end + if passed then + if is_file(path) then + target = path + break + end + elseif path == current_path then + passed = true + end + end + + local existing = state.tree:get_node(target) + if existing then + renderer.focus_node(state, target) + else + fs.navigate(state, state.path, target, nil, false) + end +end + +---@param state neotree.sources.filesystem.State +M.next_git_modified = function(state) + focus_next_git_modified(state, false) +end + +---@param state neotree.sources.filesystem.State +M.prev_git_modified = function(state) + focus_next_git_modified(state, true) +end + +M.open = function(state) + cc.open(state, utils.wrap(fs.toggle_directory, state)) +end +M.open_split = function(state) + cc.open_split(state, utils.wrap(fs.toggle_directory, state)) +end +M.open_rightbelow_vs = function(state) + cc.open_rightbelow_vs(state, utils.wrap(fs.toggle_directory, state)) +end +M.open_leftabove_vs = function(state) + cc.open_leftabove_vs(state, utils.wrap(fs.toggle_directory, state)) +end +M.open_vsplit = function(state) + cc.open_vsplit(state, utils.wrap(fs.toggle_directory, state)) +end +M.open_tabnew = function(state) + cc.open_tabnew(state, utils.wrap(fs.toggle_directory, state)) +end +M.open_drop = function(state) + cc.open_drop(state, utils.wrap(fs.toggle_directory, state)) +end +M.open_tab_drop = function(state) + cc.open_tab_drop(state, utils.wrap(fs.toggle_directory, state)) +end + +M.open_with_window_picker = function(state) + cc.open_with_window_picker(state, utils.wrap(fs.toggle_directory, state)) +end +M.split_with_window_picker = function(state) + cc.split_with_window_picker(state, utils.wrap(fs.toggle_directory, state)) +end +M.vsplit_with_window_picker = function(state) + cc.vsplit_with_window_picker(state, utils.wrap(fs.toggle_directory, state)) +end + +M.refresh = refresh + +M.rename = function(state) + cc.rename(state, utils.wrap(refresh, state)) +end + +---@param state neotree.sources.filesystem.State +M.set_root = function(state) + if state.search_pattern then + fs.reset_search(state, false) + end + + local node = state.tree:get_node() + while node and node.type ~= "directory" do + local parent_id = node:get_parent_id() + node = parent_id and state.tree:get_node(parent_id) or nil + end + + if not node then + return + end + + fs._navigate_internal(state, node:get_id(), nil, nil, false) +end + +---Toggles whether hidden files are shown or not. +---@param state neotree.sources.filesystem.State +M.toggle_hidden = function(state) + state.filtered_items.visible = not state.filtered_items.visible + log.info("Toggling hidden files: " .. tostring(state.filtered_items.visible)) + refresh(state) +end + +---Toggles whether the tree is filtered by gitignore or not. +---@param state neotree.sources.filesystem.State +M.toggle_gitignore = function(state) + log.warn("`toggle_gitignore` has been removed, running toggle_hidden instead.") + M.toggle_hidden(state) +end + +M.toggle_node = function(state) + cc.toggle_node(state, utils.wrap(fs.toggle_directory, state)) +end + +cc._add_common_commands(M) + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/components.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/components.lua new file mode 100644 index 0000000..42239e1 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/components.lua @@ -0,0 +1,49 @@ +-- This file contains the built-in components. Each componment is a function +-- that takes the following arguments: +-- config: A table containing the configuration provided by the user +-- when declaring this component in their renderer config. +-- node: A NuiNode object for the currently focused node. +-- state: The current state of the source providing the items. +-- +-- The function should return either a table, or a list of tables, each of which +-- contains the following keys: +-- text: The text to display for this item. +-- highlight: The highlight group to apply to this text. + +local highlights = require("neo-tree.ui.highlights") +local common = require("neo-tree.sources.common.components") + +---@alias neotree.Component.Filesystem._Key +---|"current_filter" + +---@class neotree.Component.Filesystem +---@field [1] neotree.Component.Filesystem._Key|neotree.Component.Common._Key + +---@type table +local M = {} + +---@class (exact) neotree.Component.Filesystem.CurrentFilter : neotree.Component.Common.CurrentFilter + +---@param config neotree.Component.Filesystem.CurrentFilter +M.current_filter = function(config, node, state) + local filter = node.search_pattern or "" + if filter == "" then + return {} + end + return { + { + text = "Find", + highlight = highlights.DIM_TEXT, + }, + { + text = string.format('"%s"', filter), + highlight = config.highlight or highlights.FILTER_TERM, + }, + { + text = "in", + highlight = highlights.DIM_TEXT, + }, + } +end + +return vim.tbl_deep_extend("force", common, M) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/init.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/init.lua new file mode 100644 index 0000000..f223f34 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/init.lua @@ -0,0 +1,536 @@ +--This file should have all functions that are in the public api and either set +--or read the state of this source. + +local utils = require("neo-tree.utils") +local _compat = require("neo-tree.utils._compat") +local fs_scan = require("neo-tree.sources.filesystem.lib.fs_scan") +local renderer = require("neo-tree.ui.renderer") +local events = require("neo-tree.events") +local log = require("neo-tree.log") +local manager = require("neo-tree.sources.manager") +local git = require("neo-tree.git") +local glob = require("neo-tree.sources.filesystem.lib.globtopattern") + +---@class neotree.sources.filesystem : neotree.Source +local M = { + name = "filesystem", + display_name = " 󰉓 Files ", +} + +local wrap = function(func) + return utils.wrap(func, M.name) +end + +---@return neotree.sources.filesystem.State +local get_state = function(tabid) + return manager.get_state(M.name, tabid) --[[@as neotree.sources.filesystem.State]] +end + +local follow_internal = function(callback, force_show, async) + log.trace("follow called") + local state = get_state() + if vim.bo.filetype == "neo-tree" or vim.bo.filetype == "neo-tree-popup" then + return false + end + local path_to_reveal = utils.normalize_path(manager.get_path_to_reveal() or "") + if not utils.truthy(path_to_reveal) then + return false + end + ---@cast path_to_reveal string + + if state.current_position == "float" then + return false + end + if not state.path then + return false + end + local window_exists = renderer.window_exists(state) + if window_exists then + local node = state.tree and state.tree:get_node() + if node then + if node:get_id() == path_to_reveal then + -- already focused + return false + end + end + else + if not force_show then + return false + end + end + + local is_in_path = path_to_reveal:sub(1, #state.path) == state.path + if not is_in_path then + return false + end + + log.debug("follow file: ", path_to_reveal) + local show_only_explicitly_opened = function() + state.explicitly_opened_nodes = state.explicitly_opened_nodes or {} + local expanded_nodes = renderer.get_expanded_nodes(state.tree) + local state_changed = false + for _, id in ipairs(expanded_nodes) do + if not state.explicitly_opened_nodes[id] then + if path_to_reveal:sub(1, #id) == id then + state.explicitly_opened_nodes[id] = state.follow_current_file.leave_dirs_open + else + local node = state.tree:get_node(id) + if node then + node:collapse() + state_changed = true + end + end + end + if state_changed then + renderer.redraw(state) + end + end + end + + fs_scan.get_items(state, nil, path_to_reveal, function() + show_only_explicitly_opened() + renderer.focus_node(state, path_to_reveal, true) + if type(callback) == "function" then + callback() + end + end, async) + return true +end + +M.follow = function(callback, force_show) + if vim.fn.bufname(0) == "COMMIT_EDITMSG" then + return false + end + if utils.is_floating() then + return false + end + utils.debounce("neo-tree-follow", function() + return follow_internal(callback, force_show) + end, 100, utils.debounce_strategy.CALL_LAST_ONLY) +end + +local fs_stat = (vim.uv or vim.loop).fs_stat + +---@param state neotree.sources.filesystem.State +---@param path string? +---@param path_to_reveal string? +---@param callback function? +M._navigate_internal = function(state, path, path_to_reveal, callback, async) + log.trace("navigate_internal", state.current_position, path, path_to_reveal) + state.dirty = false + local is_search = utils.truthy(state.search_pattern) + local path_changed = false + if not path and not state.bind_to_cwd then + path = state.path + end + if path == nil then + log.debug("navigate_internal: path is nil, using cwd") + path = manager.get_cwd(state) + end + path = utils.normalize_path(path) + + -- if path doesn't exist, navigate upwards until it does + local orig_path = path + local backed_out = false + while not fs_stat(path) do + log.debug(("navigate_internal: path %s didn't exist, going up a directory"):format(path)) + backed_out = true + local parent, _ = utils.split_path(path) + if not parent then + break + end + path = parent + end + + if backed_out then + log.warn(("Root path %s doesn't exist, backing out to %s"):format(orig_path, path)) + end + + if path ~= state.path then + log.debug("navigate_internal: path changed from ", state.path, " to ", path) + state.path = path + path_changed = true + end + + if path_to_reveal then + renderer.position.set(state, path_to_reveal) + log.debug("navigate_internal: in path_to_reveal, state.position=", state.position.node_id) + fs_scan.get_items(state, nil, path_to_reveal, callback) + else + local is_current = state.current_position == "current" + local follow_file = state.follow_current_file.enabled + and not is_search + and not is_current + and manager.get_path_to_reveal() + local handled = false + if utils.truthy(follow_file) then + handled = follow_internal(callback, true, async) + end + if not handled then + local success, msg = pcall(renderer.position.save, state) + if success then + log.trace("navigate_internal: position saved") + else + log.trace("navigate_internal: FAILED to save position: ", msg) + end + fs_scan.get_items(state, nil, nil, callback, async) + end + end + + if path_changed and state.bind_to_cwd then + manager.set_cwd(state) + end + local config = require("neo-tree").config + if config.enable_git_status and not is_search and config.git_status_async then + git.status_async(state.path, state.git_base, config.git_status_async_options) + end +end + +---Navigate to the given path. +---@param path string? Path to navigate to. If empty, will navigate to the cwd. +---@param path_to_reveal string? Node to focus after the items are loaded. +---@param callback function? Callback to call after the items are loaded. +M.navigate = function(state, path, path_to_reveal, callback, async) + state._ready = false + log.trace("navigate", path, path_to_reveal, async) + utils.debounce("filesystem_navigate", function() + M._navigate_internal(state, path, path_to_reveal, callback, async) + end, 100, utils.debounce_strategy.CALL_FIRST_AND_LAST) +end + +---@param state neotree.State +M.reset_search = function(state, refresh, open_current_node) + log.trace("reset_search") + -- Cancel any pending search + require("neo-tree.sources.filesystem.lib.filter_external").cancel() + -- reset search state + state.fuzzy_finder_mode = nil + state.use_fzy = nil + state.fzy_sort_result_scores = nil + state.sort_function_override = nil + + if refresh == nil then + refresh = true + end + if state.open_folders_before_search then + state.force_open_folders = vim.deepcopy(state.open_folders_before_search, _compat.noref()) + else + state.force_open_folders = nil + end + state.search_pattern = nil + state.open_folders_before_search = nil + if open_current_node then + local success, node = pcall(state.tree.get_node, state.tree) + if success and node then + local path = node:get_id() + renderer.position.set(state, path) + if node.type == "directory" then + path = utils.remove_trailing_slash(path) + log.trace("opening directory from search: ", path) + M.navigate(state, nil, path, function() + pcall(renderer.focus_node, state, path, false) + end) + else + utils.open_file(state, path) + if + refresh + and state.current_position ~= "current" + and state.current_position ~= "float" + then + M.navigate(state, nil, path) + end + end + end + else + if refresh then + M.navigate(state) + end + end +end + +M.show_new_children = function(state, node_or_path) + local node = node_or_path + if node_or_path == nil then + node = state.tree:get_node() + node_or_path = node:get_id() + elseif type(node_or_path) == "string" then + node = state.tree:get_node(node_or_path) + if node == nil then + local parent_path, _ = utils.split_path(node_or_path) + node = state.tree:get_node(parent_path) + if node == nil then + M.navigate(state, nil, node_or_path) + return + end + end + else + node = node_or_path + node_or_path = node:get_id() + end + + if node.type ~= "directory" then + return + end + + M.navigate(state, nil, node_or_path) +end + +M.focus_destination_children = function(state, move_from, destination) + return M.show_new_children(state, destination) +end + +---@alias neotree.Config.Cwd "tab"|"window"|"global" + +---@class neotree.Config.Filesystem.CwdTarget +---@field sidebar neotree.Config.Cwd? +---@field current neotree.Config.Cwd? + +---@class neotree.Config.Filesystem.FilteredItems +---@field visible boolean? +---@field force_visible_in_empty_folder boolean? +---@field children_inherit_highlights boolean? +---@field show_hidden_count boolean? +---@field hide_dotfiles boolean? +---@field hide_gitignored boolean? +---@field hide_hidden boolean? +---@field hide_by_name string[]? +---@field hide_by_pattern string[]? +---@field always_show string[]? +---@field always_show_by_pattern string[]? +---@field never_show string[]? +---@field never_show_by_pattern string[]? + +---@alias neotree.Config.Filesystem.FindArgsHandler fun(cmd:string, path:string, search_term:string, args:string[]):string[] + +---@class neotree.Config.Filesystem.FollowCurrentFile +---@field enabled boolean? +---@field leave_dirs_open boolean? + +---@alias neotree.Config.HijackNetrwBehavior +---|"open_default" # opening a directory opens neo-tree with the default window.position. +---|"open_current" # opening a directory opens neo-tree within the current window. +---|"disabled" # opening a directory opens neo-tree within the current window. + +---@class neotree.Config.Filesystem.Renderers : neotree.Config.Renderers + +---@class neotree.Config.Filesystem.Window : neotree.Config.Window +---@field fuzzy_finder_mappings neotree.Config.FuzzyFinder.Mappings? + +---@alias neotree.Config.Filesystem.AsyncDirectoryScan +---|"auto" +---|"always" +---|"never" + +---@alias neotree.Config.Filesystem.ScanMode +---|"shallow" +---|"deep" + +---@class (exact) neotree.Config.Filesystem : neotree.Config.Source +---@field async_directory_scan neotree.Config.Filesystem.AsyncDirectoryScan? +---@field scan_mode neotree.Config.Filesystem.ScanMode? +---@field bind_to_cwd boolean? +---@field cwd_target neotree.Config.Filesystem.CwdTarget? +---@field check_gitignore_in_search boolean? +---@field filtered_items neotree.Config.Filesystem.FilteredItems? +---@field find_by_full_path_words boolean? +---@field find_command string? +---@field find_args table|neotree.Config.Filesystem.FindArgsHandler|nil +---@field group_empty_dirs boolean? +---@field search_limit integer? +---@field follow_current_file neotree.Config.Filesystem.FollowCurrentFile? +---@field hijack_netrw_behavior neotree.Config.HijackNetrwBehavior? +---@field use_libuv_file_watcher boolean? +---@field renderers neotree.Config.Filesystem.Renderers? +---@field window neotree.Config.Filesystem.Window? +---@field enable_git_status boolean? + +---Configures the plugin, should be called before the plugin is used. +---@param config neotree.Config.Filesystem Configuration table containing any keys that the user wants to change from the defaults. May be empty to accept default values. +---@param global_config neotree.Config.Base +M.setup = function(config, global_config) + config.filtered_items = config.filtered_items or {} + config.enable_git_status = config.enable_git_status or global_config.enable_git_status + + for _, key in ipairs({ "hide_by_pattern", "always_show_by_pattern", "never_show_by_pattern" }) do + local list = config.filtered_items[key] + if type(list) == "table" then + for i, pattern in ipairs(list) do + list[i] = glob.globtopattern(pattern) + end + end + end + + for _, key in ipairs({ "hide_by_name", "always_show", "never_show" }) do + local list = config.filtered_items[key] + if type(list) == "table" then + config.filtered_items[key] = utils.list_to_dict(list) + end + end + + --Configure events for before_render + if config.before_render then + --convert to new event system + manager.subscribe(M.name, { + event = events.BEFORE_RENDER, + handler = function(state) + local this_state = get_state() + if state == this_state then + config.before_render(this_state) + end + end, + }) + elseif global_config.enable_git_status and global_config.git_status_async then + manager.subscribe(M.name, { + event = events.GIT_STATUS_CHANGED, + handler = wrap(manager.git_status_changed), + }) + elseif global_config.enable_git_status then + manager.subscribe(M.name, { + event = events.BEFORE_RENDER, + handler = function(state) + local this_state = get_state() + if state == this_state then + state.git_status_lookup = git.status(state.git_base) + end + end, + }) + end + + -- Respond to git events from git_status source or Fugitive + if global_config.enable_git_status then + manager.subscribe(M.name, { + event = events.GIT_EVENT, + handler = function() + manager.refresh(M.name) + end, + }) + end + + --Configure event handlers for file changes + if config.use_libuv_file_watcher then + manager.subscribe(M.name, { + event = events.FS_EVENT, + handler = wrap(manager.refresh), + }) + else + require("neo-tree.sources.filesystem.lib.fs_watch").unwatch_all() + if global_config.enable_refresh_on_write then + manager.subscribe(M.name, { + event = events.VIM_BUFFER_CHANGED, + handler = function(arg) + local afile = arg.afile or "" + if utils.is_real_file(afile) then + log.trace("refreshing due to vim_buffer_changed event: ", afile) + manager.refresh("filesystem") + else + log.trace("Ignoring vim_buffer_changed event for non-file: ", afile) + end + end, + }) + end + end + + --Configure event handlers for cwd changes + if config.bind_to_cwd then + manager.subscribe(M.name, { + event = events.VIM_DIR_CHANGED, + handler = wrap(manager.dir_changed), + }) + end + + --Configure event handlers for lsp diagnostic updates + if global_config.enable_diagnostics then + manager.subscribe(M.name, { + event = events.VIM_DIAGNOSTIC_CHANGED, + handler = wrap(manager.diagnostics_changed), + }) + end + + --Configure event handlers for modified files + if global_config.enable_modified_markers then + manager.subscribe(M.name, { + event = events.VIM_BUFFER_MODIFIED_SET, + handler = wrap(manager.opened_buffers_changed), + }) + end + + if global_config.enable_opened_markers then + for _, event in ipairs({ events.VIM_BUFFER_ADDED, events.VIM_BUFFER_DELETED }) do + manager.subscribe(M.name, { + event = event, + handler = wrap(manager.opened_buffers_changed), + }) + end + end + + -- Configure event handler for follow_current_file option + if config.follow_current_file.enabled then + manager.subscribe(M.name, { + event = events.VIM_BUFFER_ENTER, + handler = function(args) + if utils.is_real_file(args.afile) then + M.follow() + end + end, + }) + end +end + +---Expands or collapses the current node. +---@param state neotree.sources.filesystem.State +---@param node NuiTree.Node +---@param path_to_reveal string +---@param skip_redraw boolean? +---@param recursive boolean? +---@param callback function? +M.toggle_directory = function(state, node, path_to_reveal, skip_redraw, recursive, callback) + local tree = state.tree + if not node then + node = assert(tree:get_node()) + end + if node.type ~= "directory" then + return + end + state.explicitly_opened_nodes = state.explicitly_opened_nodes or {} + if node.loaded == false then + local id = node:get_id() + state.explicitly_opened_nodes[id] = true + renderer.position.set(state, nil) + fs_scan.get_items(state, id, path_to_reveal, callback, false, recursive) + elseif node:has_children() then + local updated = false + if node:is_expanded() then + updated = node:collapse() + state.explicitly_opened_nodes[node:get_id()] = false + else + updated = node:expand() + state.explicitly_opened_nodes[node:get_id()] = true + end + if updated and not skip_redraw then + renderer.redraw(state) + end + if path_to_reveal then + renderer.focus_node(state, path_to_reveal) + end + elseif require("neo-tree").config.filesystem.scan_mode == "deep" then + node.empty_expanded = not node.empty_expanded + renderer.redraw(state) + end +end + +M.prefetcher = { + ---@param state neotree.sources.filesystem.State + ---@param node NuiTree.Node + prefetch = function(state, node) + if node.type ~= "directory" then + return + end + log.debug("Running fs prefetch for: " .. node:get_id()) + fs_scan.get_dir_items_async(state, node:get_id(), true) + end, + should_prefetch = function(node) + return not node.loaded + end, +} + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/filter.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/filter.lua new file mode 100644 index 0000000..d215034 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/filter.lua @@ -0,0 +1,247 @@ +-- This file holds all code for the search function. + +local Input = require("nui.input") +local fs = require("neo-tree.sources.filesystem") +local popups = require("neo-tree.ui.popups") +local renderer = require("neo-tree.ui.renderer") +local utils = require("neo-tree.utils") +local log = require("neo-tree.log") +local manager = require("neo-tree.sources.manager") +local compat = require("neo-tree.utils._compat") +local common_filter = require("neo-tree.sources.common.filters") + +local M = {} + +---@param state neotree.sources.filesystem.State +---@param search_as_you_type boolean? +---@param fuzzy_finder_mode "directory"|boolean? +---@param use_fzy boolean? +---@param keep_filter_on_submit boolean? +M.show_filter = function( + state, + search_as_you_type, + fuzzy_finder_mode, + use_fzy, + keep_filter_on_submit +) + local popup_options + local winid = vim.api.nvim_get_current_win() + local height = vim.api.nvim_win_get_height(winid) + local scroll_padding = 3 + local popup_msg = "Search:" + + if search_as_you_type then + if fuzzy_finder_mode == "directory" then + popup_msg = "Filter Directories:" + else + popup_msg = "Filter:" + end + end + if state.config.title then + popup_msg = state.config.title + end + if state.current_position == "float" then + scroll_padding = 0 + local width = vim.fn.winwidth(winid) + local row = height - 2 + vim.api.nvim_win_set_height(winid, row) + popup_options = popups.popup_options(popup_msg, width, { + relative = "win", + winid = winid, + position = { + row = row, + col = 0, + }, + size = width, + }) + else + local width = vim.fn.winwidth(0) - 2 + local row = height - 3 + popup_options = popups.popup_options(popup_msg, width, { + relative = "win", + winid = winid, + position = { + row = row, + col = 0, + }, + size = width, + }) + end + + ---@type neotree.Config.SortFunction + local sort_by_score = function(a, b) + -- `state.fzy_sort_result_scores` should be defined in + -- `sources.filesystem.lib.filter_external.fzy_sort_files` + local result_scores = state.fzy_sort_result_scores or { foo = 0, baz = 0 } + local a_score = result_scores[a.path] + local b_score = result_scores[b.path] + if a_score == nil or b_score == nil then + log.debug( + string.format([[Fzy: failed to compare %s: %s, %s: %s]], a.path, a_score, b.path, b_score) + ) + local config = require("neo-tree").config + if config.sort_function ~= nil then + return config.sort_function(a, b) + end + return nil + end + return a_score > b_score + end + + local select_first_file = function() + local is_file = function(node) + return node.type == "file" + end + local files = renderer.select_nodes(state.tree, is_file, 1) + if #files > 0 then + renderer.focus_node(state, files[1]:get_id(), true) + end + end + + local has_pre_search_folders = utils.truthy(state.open_folders_before_search) + if not has_pre_search_folders then + log.trace("No search or pre-search folders, recording pre-search folders now") + ---@type table|nil + state.open_folders_before_search = renderer.get_expanded_nodes(state.tree) + end + + local waiting_for_default_value = utils.truthy(state.search_pattern) + local input = Input(popup_options, { + prompt = " ", + default_value = state.search_pattern, + on_submit = function(value) + if value == "" then + fs.reset_search(state) + else + if search_as_you_type and fuzzy_finder_mode and not keep_filter_on_submit then + fs.reset_search(state, true, true) + return + end + state.search_pattern = value + manager.refresh("filesystem", function() + -- focus first file + local nodes = renderer.get_all_visible_nodes(state.tree) + for _, node in ipairs(nodes) do + if node.type == "file" then + renderer.focus_node(state, node:get_id(), false) + break + end + end + end) + end + end, + --this can be bad in a deep folder structure + on_change = function(value) + if not search_as_you_type then + return + end + -- apparently when a default value is set, on_change fires for every character + if waiting_for_default_value then + if #value < #state.search_pattern then + return + else + waiting_for_default_value = false + end + end + if value == state.search_pattern then + return + elseif value == nil then + return + elseif value == "" then + if state.search_pattern == nil then + return + end + log.trace("Resetting search in on_change") + local original_open_folders = nil + if type(state.open_folders_before_search) == "table" then + original_open_folders = vim.deepcopy(state.open_folders_before_search, compat.noref()) + end + fs.reset_search(state) + state.open_folders_before_search = original_open_folders + else + log.trace("Setting search in on_change to: " .. value) + state.search_pattern = value + state.fuzzy_finder_mode = fuzzy_finder_mode + if use_fzy then + state.sort_function_override = sort_by_score + state.use_fzy = true + end + ---@type function|nil + local callback = select_first_file + if fuzzy_finder_mode == "directory" then + callback = nil + end + + local len = #value + local delay = 500 + if len > 3 then + delay = 100 + elseif len > 2 then + delay = 200 + elseif len > 1 then + delay = 400 + end + + utils.debounce("filesystem_filter", function() + fs._navigate_internal(state, nil, nil, callback) + end, delay, utils.debounce_strategy.CALL_LAST_ONLY) + end + end, + }) + + input:mount() + + local restore_height = vim.schedule_wrap(function() + if vim.api.nvim_win_is_valid(winid) then + vim.api.nvim_win_set_height(winid, height) + end + end) + ---@class neotree.sources.filesystem.FuzzyFinder.BuiltinCommands : neotree.FuzzyFinder.BuiltinCommands + local cmds + cmds = { + move_cursor_down = function(_state, _scroll_padding) + renderer.focus_node(_state, nil, true, 1, _scroll_padding) + end, + + move_cursor_up = function(_state, _scroll_padding) + renderer.focus_node(_state, nil, true, -1, _scroll_padding) + vim.cmd("redraw!") + end, + + close = function(_state, _scroll_padding) + vim.cmd("stopinsert") + input:unmount() + -- If this was closed due to submit, that function will handle the reset_search + vim.defer_fn(function() + if + fuzzy_finder_mode + and utils.truthy(state.search_pattern) + and not keep_filter_on_submit + then + fs.reset_search(state, true) + end + end, 100) + restore_height() + end, + close_keep_filter = function(_state, _scroll_padding) + log.info("Persisting the search filter") + keep_filter_on_submit = true + cmds.close(_state, _scroll_padding) + end, + close_clear_filter = function(_state, _scroll_padding) + log.info("Clearing the search filter") + keep_filter_on_submit = false + cmds.close(_state, _scroll_padding) + end, + } + + common_filter.setup_hooks(input, cmds, state, scroll_padding) + + if not fuzzy_finder_mode then + return + end + + common_filter.setup_mappings(input, cmds, state, scroll_padding) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/filter_external.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/filter_external.lua new file mode 100644 index 0000000..54a3797 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/filter_external.lua @@ -0,0 +1,393 @@ +local log = require("neo-tree.log") +local Job = require("plenary.job") +local utils = require("neo-tree.utils") +local Queue = require("neo-tree.collections").Queue + +local M = {} +local fd_supports_max_results = nil + +local test_for_max_results = function(cmd) + if fd_supports_max_results == nil then + if cmd == "fd" or cmd == "fdfind" then + --test if it supports the max-results option + local test = vim.fn.system(cmd .. " this_is_only_a_test --max-depth=1 --max-results=1") + if test:match("^error:") then + fd_supports_max_results = false + log.debug(cmd, "does NOT support max-results") + else + fd_supports_max_results = true + log.debug(cmd, "supports max-results") + end + end + end +end + +local get_find_command = function(state) + if state.find_command then + test_for_max_results(state.find_command) + return state.find_command + end + + if 1 == vim.fn.executable("fdfind") then + state.find_command = "fdfind" + elseif 1 == vim.fn.executable("fd") then + state.find_command = "fd" + elseif 1 == vim.fn.executable("find") and vim.fn.has("win32") == 0 then + state.find_command = "find" + elseif 1 == vim.fn.executable("where") then + state.find_command = "where" + end + + test_for_max_results(state.find_command) + return state.find_command +end + +local running_jobs = Queue:new() +local kill_job = function(job) + local pid = job.pid + job:shutdown() + if pid ~= nil and pid > 0 then + if utils.is_windows then + vim.fn.system("taskkill /F /T /PID " .. pid) + else + vim.fn.system("kill -9 " .. pid) + end + end + return true +end + +M.cancel = function() + if running_jobs:is_empty() then + return + end + running_jobs:for_each(kill_job) +end + +---@class neotree.FileKind +---@field file boolean? +---@field directory boolean? +---@field symlink boolean? +---@field socket boolean? +---@field pipe boolean? +---@field executable boolean? +---@field empty boolean? +---@field block boolean? Only for `find` +---@field character boolean? Only for `find` + +---filter_files_external +-- Spawns a filter command based on `cmd` +---@param cmd string Command to execute. Use `get_find_command` most times. +---@param path string Base directory to start the search. +---@param glob string | nil If not nil, do glob search. Take precedence on `regex` +---@param regex string | nil If not nil, do regex search if command supports. if glob ~= nil, ignored +---@param full_path boolean If true, search agaist the absolute path +---@param kind neotree.FileKind | nil Return only true filetypes. If nil, all are returned. +---@param ignore { dotfiles: boolean?, gitignore: boolean? } If true, ignored from result. Default: false +---@param limit? integer | nil Maximim number of results. nil will return everything. +---@param find_args? string[] | table Any additional options passed to command if any. +---@param on_insert? fun(err: string, line: string): any Executed for each line of stdout and stderr. +---@param on_exit? fun(return_val: number): any Executed at the end. +M.filter_files_external = function( + cmd, + path, + glob, + regex, + full_path, + kind, + ignore, + limit, + find_args, + on_insert, + on_exit +) + if glob ~= nil and regex ~= nil then + local log_msg = string.format([[glob: %s, regex: %s]], glob, regex) + log.warn("both glob and regex are set. glob will take precedence. " .. log_msg) + end + ignore = ignore or {} + kind = kind or {} + limit = limit or math.huge -- math.huge == no limit + local file_kind_map = { + file = "f", + directory = "d", + symlink = "l", + socket = "s", + pipe = "p", + executable = "x", -- only for `fd` + empty = "e", -- only for `fd` + block = "b", -- only for `find` + character = "c", -- only for `find` + } + + local args = {} + local function append(...) + for _, v in pairs({ ... }) do + if v ~= nil then + args[#args + 1] = v + end + end + end + + local function append_find_args() + if find_args then + if type(find_args) == "string" then + append(find_args) + elseif type(find_args) == "table" then + if find_args[1] then + append(unpack(find_args)) + elseif find_args[cmd] then + append(unpack(find_args[cmd])) ---@diagnostic disable-line + end + elseif type(find_args) == "function" then + args = find_args(cmd, path, glob, args) + end + end + end + + if cmd == "fd" or cmd == "fdfind" then + if not ignore.dotfiles then + append("--hidden") + end + if not ignore.gitignore then + append("--no-ignore") + end + append("--color", "never") + if fd_supports_max_results and 0 < limit and limit < math.huge then + append("--max-results", limit) + end + for k, v in pairs(kind) do + if v and file_kind_map[k] ~= nil then + append("--type", k) + end + end + if full_path then + append("--full-path") + if glob ~= nil then + local words = utils.split(glob, " ") + regex = ".*" .. table.concat(words, ".*") .. ".*" + glob = nil + end + end + if glob ~= nil then + append("--glob") + end + append_find_args() + append("--", glob or regex or "") + append(path) + elseif cmd == "find" then + append(path) + local file_kinds = {} + for k, v in pairs(kind) do + if v and file_kind_map[k] ~= nil then + file_kinds[#file_kinds + 1] = file_kind_map[k] + end + end + if ignore.dotfiles then + append("-name", ".*", "-prune", "-o") + end + if #file_kinds > 0 then + append("-type", table.concat(file_kinds, ",")) + end + if kind.empty then + append("-empty") + end + if kind.executable then + append("-executable") + end + if glob ~= nil and not full_path then + append("-iname", glob) + elseif glob ~= nil and full_path then + local words = utils.split(glob, " ") + regex = ".*" .. table.concat(words, ".*") .. ".*" + append("-regextype", "sed", "-regex", regex) + elseif regex ~= nil then + append("-regextype", "sed", "-regex", regex) + end + append("-print") + append_find_args() + elseif cmd == "fzf" then + -- This does not work yet, there's some kind of issue with how fzf uses stdout + error("fzf is not a supported find_command") + append_find_args() + append("--no-sort", "--no-expect", "--filter", glob or regex) -- using the raw term without glob patterns + elseif cmd == "where" then + append_find_args() + append("/r", path, glob or regex) + else + return { "No search command found!" } + end + + if fd_supports_max_results then + limit = math.huge -- `fd` manages limit on its own + end + local item_count = 0 + ---@diagnostic disable-next-line: missing-fields + local job = Job:new({ + command = cmd, + cwd = path, + args = args, + enable_recording = false, + on_stdout = function(err, line) + if item_count < limit and on_insert then + on_insert(err, line) + item_count = item_count + 1 + end + end, + on_stderr = function(err, line) + if item_count < limit and on_insert then + on_insert(err or line, line) + -- item_count = item_count + 1 + end + end, + on_exit = function(_, return_val) + if on_exit then + on_exit(return_val) + end + end, + }) + + -- This ensures that only one job is running at a time + running_jobs:for_each(kill_job) + running_jobs:add(job) + job:start() +end + +local function fzy_sort_get_total_score(terms, path) + local fzy = require("neo-tree.sources.common.filters.filter_fzy") + local total_score = 0 + for _, term in ipairs(terms) do -- spaces in `opts.term` are treated as `and` + local score = fzy.score(term, path) + if score == fzy.get_score_min() then -- if any not found, end searching + return 0 + end + total_score = total_score + score + end + return total_score +end + +local function modify_parent_scores(result_scores, path, score) + local parent, _ = utils.split_path(path) + while parent ~= nil do -- back propagate the score to its ancesters + if score > (result_scores[parent] or 0) then + result_scores[parent] = score + parent, _ = utils.split_path(parent) + else + break + end + end +end + +---@param state neotree.State +M.fzy_sort_files = function(opts, state) + state = state or {} + local filters = opts.filtered_items + local limit = opts.limit or 100 + local full_path_words = opts.find_by_full_path_words + local fuzzy_finder_mode = opts.fuzzy_finder_mode + local pwd = opts.path + if pwd:sub(-1) ~= "/" then + pwd = pwd .. "/" + end + local pwd_length = #pwd + local terms = {} + for term in string.gmatch(opts.term, "[^%s]+") do -- space split opts.term + terms[#terms + 1] = term + end + + -- The base search is anything that contains the characters in the term + -- The fzy score is then used to sort the results + local chars = {} + local regex = ".*" + local chars_to_escape = + { "%", "+", "-", "?", "[", "^", "$", "(", ")", "{", "}", "=", "!", "<", ">", "|", ":", "#" } + for _, term in ipairs(terms) do + for c in term:gmatch(".") do + if not chars[c] then + chars[c] = true + if chars_to_escape[c] then + c = [[\]] .. c + end + regex = regex .. c .. ".*" + end + end + end + + local result_counter = 0 + + local index = 1 + state.fzy_sort_result_scores = {} + local function on_insert(err, path) + if not err then + local relative_path = path + if not full_path_words and #path > pwd_length and path:sub(1, pwd_length) == pwd then + relative_path = "./" .. path:sub(pwd_length + 1) + end + index = index + 1 + if state.fzy_sort_result_scores == nil then + state.fzy_sort_result_scores = {} + end + state.fzy_sort_result_scores[path] = 0 + local score = fzy_sort_get_total_score(terms, relative_path) + if score > 0 then + state.fzy_sort_result_scores[path] = score + result_counter = result_counter + 1 + modify_parent_scores(state.fzy_sort_result_scores, path, score) + opts.on_insert(nil, path) + if result_counter >= limit then + vim.schedule(M.cancel) + end + end + end + end + + M.filter_files_external( + get_find_command(state), + pwd, + nil, + regex, + true, + { directory = fuzzy_finder_mode == "directory", file = fuzzy_finder_mode ~= "directory" }, + { + dotfiles = not filters.visible and filters.hide_dotfiles, + gitignore = not filters.visible and filters.hide_gitignored, + }, + nil, + opts.find_args, + on_insert, + opts.on_exit + ) +end + +M.find_files = function(opts) + local filters = opts.filtered_items + local full_path_words = opts.find_by_full_path_words + local regex, glob = nil, nil + local fuzzy_finder_mode = opts.fuzzy_finder_mode + + glob = opts.term + if glob:sub(1) ~= "*" then + glob = "*" .. glob + end + if glob:sub(-1) ~= "*" then + glob = glob .. "*" + end + + M.filter_files_external( + get_find_command(opts), + opts.path, + glob, + regex, + full_path_words, + { directory = fuzzy_finder_mode == "directory" }, + { + dotfiles = not filters.visible and filters.hide_dotfiles, + gitignore = not filters.visible and filters.hide_gitignored, + }, + opts.limit or 200, + opts.find_args, + opts.on_insert, + opts.on_exit + ) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/fs_actions.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/fs_actions.lua new file mode 100644 index 0000000..3f18262 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/fs_actions.lua @@ -0,0 +1,719 @@ +-- This file is for functions that mutate the filesystem. + +-- This code started out as a copy from: +-- https://github.com/mhartington/dotfiles +-- and modified to fit neo-tree's api. +-- Permalink: https://github.com/mhartington/dotfiles/blob/7560986378753e0c047d940452cb03a3b6439b11/config/nvim/lua/mh/filetree/init.lua +local api = vim.api +local uv = vim.uv or vim.loop +local scan = require("plenary.scandir") +local utils = require("neo-tree.utils") +local inputs = require("neo-tree.ui.inputs") +local events = require("neo-tree.events") +local log = require("neo-tree.log") +local Path = require("plenary").path + +local M = {} + +---@param a uv.fs_stat.result? +---@param b uv.fs_stat.result? +---@return boolean equal Whether a and b are stats of the same file +local same_file = function(a, b) + return a and b and a.dev == b.dev and a.ino == b.ino or false +end + +---Checks to see if a file can safely be renamed to its destination without data loss. +---Also prevents renames from going through if the rename will not do anything. +---Has an additional check for case-insensitive filesystems (e.g. for windows) +---@param source string +---@param destination string +---@return boolean rename_is_safe +local function rename_is_safe(source, destination) + local destination_file = uv.fs_stat(destination) + if not destination_file then + return true + end + + local src = utils.normalize_path(source) + local dest = utils.normalize_path(destination) + local changing_casing = src ~= dest and src:lower() == dest:lower() + if changing_casing then + local src_file = uv.fs_stat(src) + -- We check that the two paths resolve to the same canonical filename and file. + return same_file(src_file, destination_file) + and uv.fs_realpath(src) == uv.fs_realpath(destination) + end + return false +end + +local function find_replacement_buffer(for_buf) + local bufs = vim.api.nvim_list_bufs() + + -- make sure the alternate buffer is at the top of the list + local alt = vim.fn.bufnr("#") + if alt ~= -1 and alt ~= for_buf then + table.insert(bufs, 1, alt) + end + + -- find the first valid real file buffer + for _, buf in ipairs(bufs) do + if buf ~= for_buf then + local is_valid = vim.api.nvim_buf_is_valid(buf) + if is_valid then + local buftype = vim.bo[buf].buftype + if buftype == "" then + return buf + end + end + end + end + return -1 +end + +local function clear_buffer(path) + local buf = utils.find_buffer_by_name(path) + if buf < 1 then + return + end + local alt = find_replacement_buffer(buf) + -- Check all windows to see if they are using the buffer + for _, win in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == buf then + -- if there is no alternate buffer yet, create a blank one now + if alt < 1 or alt == buf then + alt = vim.api.nvim_create_buf(true, false) + end + -- replace the buffer displayed in this window with the alternate buffer + vim.api.nvim_win_set_buf(win, alt) + end + end + local success, msg = pcall(vim.api.nvim_buf_delete, buf, { force = true }) + if not success then + log.error("Could not clear buffer: ", msg) + end +end + +---Opens new_buf in each window that has old_buf currently open. +---Useful during file rename. +---@param old_buf number +---@param new_buf number +local function replace_buffer_in_windows(old_buf, new_buf) + for _, win in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == old_buf then + vim.api.nvim_win_set_buf(win, new_buf) + end + end +end + +local function rename_buffer(old_path, new_path) + local force_save = function() + vim.cmd("silent! write!") + end + + for _, buf in pairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_is_loaded(buf) then + local buf_name = vim.api.nvim_buf_get_name(buf) + local new_buf_name = nil + if old_path == buf_name then + new_buf_name = new_path + elseif utils.is_subpath(old_path, buf_name) then + new_buf_name = new_path .. buf_name:sub(#old_path + 1) + end + if utils.truthy(new_buf_name) then + local new_buf = vim.fn.bufadd(new_buf_name) + vim.fn.bufload(new_buf) + vim.bo[new_buf].buflisted = true + replace_buffer_in_windows(buf, new_buf) + + if vim.bo[buf].buftype == "" then + local modified = vim.bo[buf].modified + if modified then + local old_buffer_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + vim.api.nvim_buf_set_lines(new_buf, 0, -1, false, old_buffer_lines) + + local msg = buf_name .. " has been modified. Save under new name? (y/n) " + inputs.confirm(msg, function(confirmed) + if confirmed then + vim.api.nvim_buf_call(new_buf, force_save) + log.trace("Force saving renamed buffer with changes") + else + vim.cmd("echohl WarningMsg") + vim.cmd( + [[echo "Skipping force save. You'll need to save it with `:w!` when you are ready to force writing with the new name."]] + ) + vim.cmd("echohl NONE") + end + end) + end + end + vim.api.nvim_buf_delete(buf, { force = true }) + end + end + end +end + +local function create_all_parents(path) + local function create_all_as_folders(in_path) + if not uv.fs_stat(in_path) then + local parent, _ = utils.split_path(in_path) + if parent then + create_all_as_folders(parent) + end + uv.fs_mkdir(in_path, 493) + end + end + + local parent_path, _ = utils.split_path(path) + create_all_as_folders(parent_path) +end + +-- Gets a non-existing filename from the user and executes the callback with it. +---@param source string +---@param destination string +---@param using_root_directory boolean +---@param name_chosen_callback fun(string) +---@param first_message string? +local function get_unused_name( + source, + destination, + using_root_directory, + name_chosen_callback, + first_message +) + if not rename_is_safe(source, destination) then + local parent_path, name + if not using_root_directory then + parent_path, name = utils.split_path(destination) + elseif #using_root_directory > 0 then + parent_path = destination:sub(1, #using_root_directory) + name = destination:sub(#using_root_directory + 2) + else + parent_path = nil + name = destination + end + + local message = first_message or name .. " already exists. Please enter a new name: " + inputs.input(message, name, function(new_name) + if new_name and string.len(new_name) > 0 then + local new_path = parent_path and parent_path .. utils.path_separator .. new_name or new_name + get_unused_name(source, new_path, using_root_directory, name_chosen_callback) + end + end) + else + name_chosen_callback(destination) + end +end + +-- Move Node +M.move_node = function(source, destination, callback, using_root_directory) + log.trace( + "Moving node: ", + source, + " to ", + destination, + ", using root directory: ", + using_root_directory + ) + local _, name = utils.split_path(source) + get_unused_name(source, destination or source, using_root_directory, function(dest) + -- Resolve user-inputted relative paths out of the absolute paths + dest = vim.fs.normalize(dest) + if utils.is_windows then + dest = utils.windowize_path(dest) + end + local function move_file() + create_all_parents(dest) + uv.fs_rename(source, dest, function(err) + if err then + log.error("Could not move the files from", source, "to", dest, ":", err) + return + end + vim.schedule(function() + rename_buffer(source, dest) + end) + vim.schedule(function() + events.fire_event(events.FILE_MOVED, { + source = source, + destination = dest, + }) + if callback then + callback(source, dest) + end + end) + end) + end + local event_result = events.fire_event(events.BEFORE_FILE_MOVE, { + source = source, + destination = dest, + callback = move_file, + }) or {} + if event_result.handled then + return + end + move_file() + end, 'Move "' .. name .. '" to:') +end + +---Plenary path.copy() when used to copy a recursive structure, can return a nested +-- table with for each file a Path instance and the success result. +---@param copy_result table The output of Path.copy() +---@param flat_result table Return value containing the flattened results +local function flatten_path_copy_result(flat_result, copy_result) + if not copy_result then + return + end + for k, v in pairs(copy_result) do + if type(v) == "table" then + flatten_path_copy_result(flat_result, v) + else + table.insert(flat_result, { destination = k.filename, success = v }) + end + end +end + +-- Check if all files were copied successfully, using the flattened copy result +local function check_path_copy_result(flat_result) + if not flat_result then + return + end + for _, file_result in ipairs(flat_result) do + if not file_result.success then + return false + end + end + return true +end + +-- Copy Node +M.copy_node = function(source, _destination, callback, using_root_directory) + local _, name = utils.split_path(source) + get_unused_name(source, _destination or source, using_root_directory, function(destination) + local parent_path, _ = utils.split_path(destination) + if source == parent_path then + log.warn("Cannot copy a file/folder to itself") + return + end + + local event_result = events.fire_event(events.BEFORE_FILE_ADD, destination) or {} + if event_result.handled then + return + end + + local source_path = Path:new(source) + if source_path:is_file() then + -- When the source is a file, then Path.copy() currently doesn't create + -- the potential non-existing parent directories of the destination. + create_all_parents(destination) + end + local success, result = pcall(source_path.copy, source_path, { + destination = destination, + recursive = true, + parents = true, + }) + if not success then + log.error("Could not copy the file(s) from", source, "to", destination, ":", result) + return + end + + -- It can happen that the Path.copy() function returns successfully but + -- the copy action still failed. In this case the copy() result contains + -- a nested table of Path instances for each file copied, and the success + -- result. + local flat_result = {} + flatten_path_copy_result(flat_result, result) + if not check_path_copy_result(flat_result) then + log.error("Could not copy the file(s) from", source, "to", destination, ":", flat_result) + return + end + + vim.schedule(function() + events.fire_event(events.FILE_ADDED, destination) + if callback then + callback(source, destination) + end + end) + end, 'Copy "' .. name .. '" to:') +end + +--- Create a new directory +M.create_directory = function(in_directory, callback, using_root_directory) + local base + if type(using_root_directory) == "string" then + if in_directory == using_root_directory then + base = "" + elseif #using_root_directory > 0 then + base = in_directory:sub(#using_root_directory + 2) .. utils.path_separator + else + base = in_directory .. utils.path_separator + end + else + base = vim.fn.fnamemodify(in_directory .. utils.path_separator, ":~") + using_root_directory = false + end + + inputs.input("Enter name for new directory:", base, function(destinations) + if not destinations then + return + end + + for _, destination in ipairs(utils.brace_expand(destinations)) do + if not destination or destination == base then + return + end + + if using_root_directory then + destination = utils.path_join(using_root_directory, destination) + else + destination = vim.fn.fnamemodify(destination, ":p") + end + + local event_result = events.fire_event(events.BEFORE_FILE_ADD, destination) or {} + if event_result.handled then + return + end + + if uv.fs_stat(destination) then + log.warn("Directory already exists") + return + end + + create_all_parents(destination) + uv.fs_mkdir(destination, 493) + + vim.schedule(function() + events.fire_event(events.FILE_ADDED, destination) + if callback then + callback(destination) + end + end) + end + end) +end + +--- Create Node +M.create_node = function(in_directory, callback, using_root_directory) + local base + if type(using_root_directory) == "string" then + if in_directory == using_root_directory then + base = "" + elseif #using_root_directory > 0 then + base = in_directory:sub(#using_root_directory + 2) .. utils.path_separator + else + base = in_directory .. utils.path_separator + end + else + base = vim.fn.fnamemodify(in_directory .. utils.path_separator, ":~") + using_root_directory = false + end + + local dir_ending = '"/"' + if utils.path_separator ~= "/" then + dir_ending = dir_ending .. string.format(' or "%s"', utils.path_separator) + end + local msg = "Enter name for new file or directory (dirs end with a " .. dir_ending .. "):" + inputs.input(msg, base, function(destinations) + if not destinations then + return + end + + for _, destination in ipairs(utils.brace_expand(destinations)) do + if not destination or destination == base then + return + end + local is_dir = vim.endswith(destination, "/") + or vim.endswith(destination, utils.path_separator) + + if using_root_directory then + destination = utils.path_join(using_root_directory, destination) + else + destination = vim.fn.fnamemodify(destination, ":p") + end + + destination = utils.normalize_path(destination) + if uv.fs_stat(destination) then + log.warn("File already exists") + return + end + + local complete = vim.schedule_wrap(function() + events.fire_event(events.FILE_ADDED, destination) + if callback then + callback(destination) + end + end) + local event_result = events.fire_event(events.BEFORE_FILE_ADD, destination) or {} + if event_result.handled then + complete() + return + end + + create_all_parents(destination) + if is_dir then + uv.fs_mkdir(destination, 493) + else + local open_mode = uv.constants.O_CREAT + uv.constants.O_WRONLY + uv.constants.O_TRUNC + local fd = uv.fs_open(destination, open_mode, 420) + if not fd then + if not uv.fs_stat(destination) then + log.error("Could not create file " .. destination) + return + else + log.warn("Failed to complete file creation of " .. destination) + end + else + uv.fs_close(fd) + end + end + complete() + end + end) +end + +---Recursively delete a directory and its children. +---@param dir_path string Directory to delete. +---@return boolean success Whether the directory was deleted. +local function delete_dir(dir_path) + local handle = uv.fs_scandir(dir_path) + if type(handle) == "string" then + log.error(handle) + return false + end + + if not handle then + log.error("could not scan dir " .. dir_path) + return false + end + + while true do + local child_name, t = uv.fs_scandir_next(handle) + if not child_name then + break + end + + local child_path = dir_path .. "/" .. child_name + if t == "directory" then + local success = delete_dir(child_path) + if not success then + log.error("failed to delete ", child_path) + return false + end + else + local success = uv.fs_unlink(child_path) + if not success then + return false + end + clear_buffer(child_path) + end + end + return uv.fs_rmdir(dir_path) or false +end + +-- Delete Node +M.delete_node = function(path, callback, noconfirm) + local _, name = utils.split_path(path) + local msg = string.format("Are you sure you want to delete '%s'?", name) + + log.trace("Deleting node: ", path) + local _type = "unknown" + local stat = uv.fs_stat(path) + if stat then + _type = stat.type + if _type == "link" then + local link_to = uv.fs_readlink(path) + if not link_to then + log.error("Could not read link") + return + end + local target_file = uv.fs_stat(link_to) + if target_file then + _type = target_file.type + end + _type = uv.fs_stat(link_to).type + end + if _type == "directory" then + local children = scan.scan_dir(path, { + hidden = true, + respect_gitignore = false, + add_dirs = true, + depth = 1, + }) + if #children > 0 then + msg = "WARNING: Dir not empty! " .. msg + end + end + else + log.warn("Could not read file/dir:", path, stat, ", attempting to delete anyway...") + -- Guess the type by whether it appears to have an extension + if path:match("%.(.+)$") then + _type = "file" + else + _type = "directory" + end + return + end + + local do_delete = function() + local complete = vim.schedule_wrap(function() + events.fire_event(events.FILE_DELETED, path) + if callback then + callback(path) + end + end) + + local event_result = events.fire_event(events.BEFORE_FILE_DELETE, path) or {} + if event_result.handled then + complete() + return + end + + if _type == "directory" then + -- first try using native system commands, which are recursive + local success = false + if utils.is_windows then + local result = + vim.fn.system({ "cmd.exe", "/c", "rmdir", "/s", "/q", vim.fn.shellescape(path) }) + local error = vim.v.shell_error + if error ~= 0 then + log.debug("Could not delete directory '", path, "' with rmdir: ", result) + else + log.info("Deleted directory ", path) + success = true + end + else + local result = vim.fn.system({ "rm", "-Rf", path }) + local error = vim.v.shell_error + if error ~= 0 then + log.debug("Could not delete directory '", path, "' with rm: ", result) + else + log.info("Deleted directory ", path) + success = true + end + end + -- Fallback to using libuv if native commands fail + if not success then + success = delete_dir(path) + if not success then + return log.error("Could not remove directory: " .. path) + end + end + else + local success = uv.fs_unlink(path) + if not success then + return log.error("Could not remove file: " .. path) + end + clear_buffer(path) + end + complete() + end + + if noconfirm then + do_delete() + else + inputs.confirm(msg, function(confirmed) + if confirmed then + do_delete() + end + end) + end +end + +M.delete_nodes = function(paths_to_delete, callback) + local msg = "Are you sure you want to delete " .. #paths_to_delete .. " items?" + inputs.confirm(msg, function(confirmed) + if not confirmed then + return + end + + for _, path in ipairs(paths_to_delete) do + M.delete_node(path, nil, true) + end + + if callback then + vim.schedule(function() + callback(paths_to_delete[#paths_to_delete]) + end) + end + end) +end + +local rename_node = function(msg, name, get_destination, path, callback) + inputs.input(msg, name, function(new_name) + -- If cancelled + if not new_name or new_name == "" then + log.info("Operation canceled") + return + end + + local destination = get_destination(new_name) + + if not rename_is_safe(path, destination) then + log.warn(destination, " already exists, canceling") + return + end + + local complete = vim.schedule_wrap(function() + rename_buffer(path, destination) + events.fire_event(events.FILE_RENAMED, { + source = path, + destination = destination, + }) + if callback then + callback(path, destination) + end + log.info("Renamed " .. new_name .. " successfully") + end) + + local function fs_rename() + uv.fs_rename(path, destination, function(err) + if err then + log.warn("Could not rename the files") + return + end + complete() + end) + end + + local event_result = events.fire_event(events.BEFORE_FILE_RENAME, { + source = path, + destination = destination, + callback = fs_rename, + }) or {} + if event_result.handled then + complete() + return + end + fs_rename() + end) +end + +-- Rename Node +M.rename_node = function(path, callback) + local parent_path, name = utils.split_path(path) + local msg = string.format('Enter new name for "%s":', name) + + local get_destination = function(new_name) + return parent_path .. utils.path_separator .. new_name + end + + rename_node(msg, name, get_destination, path, callback) +end + +-- Rename Node Base Name +M.rename_node_basename = function(path, callback) + local parent_path, name = utils.split_path(path) + local base_name = vim.fn.fnamemodify(path, ":t:r") + local extension = vim.fn.fnamemodify(path, ":e") + + local msg = string.format('Enter new base name for "%s":', name) + + local get_destination = function(new_base_name) + return parent_path + .. utils.path_separator + .. new_base_name + .. (extension:len() == 0 and "" or "." .. extension) + end + + rename_node(msg, base_name, get_destination, path, callback) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/fs_scan.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/fs_scan.lua new file mode 100644 index 0000000..6b67a8a --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/fs_scan.lua @@ -0,0 +1,738 @@ +-- This files holds code for scanning the filesystem to build the tree. +local uv = vim.uv or vim.loop + +local renderer = require("neo-tree.ui.renderer") +local utils = require("neo-tree.utils") +local filter_external = require("neo-tree.sources.filesystem.lib.filter_external") +local file_items = require("neo-tree.sources.common.file-items") +local file_nesting = require("neo-tree.sources.common.file-nesting") +local log = require("neo-tree.log") +local fs_watch = require("neo-tree.sources.filesystem.lib.fs_watch") +local git = require("neo-tree.git") +local events = require("neo-tree.events") +local async = require("plenary.async") + +local M = {} + +--- how many entries to load per readdir +local ENTRIES_BATCH_SIZE = 1000 + +---@param context neotree.sources.filesystem.Context +---@param dir_path string +local on_directory_loaded = function(context, dir_path) + local state = context.state + local scanned_folder = context.folders[dir_path] + if scanned_folder then + scanned_folder.loaded = true + end + if state.use_libuv_file_watcher then + local root = context.folders[dir_path] + if root then + local target_path = root.is_link and root.link_to or root.path + local fs_watch_callback = vim.schedule_wrap(function(err, fname) + if err then + log.error("file_event_callback: ", err) + return + end + if context.is_a_never_show_file(fname) then + -- don't fire events for nodes that are designated as "never show" + return + else + events.fire_event(events.FS_EVENT, { afile = target_path }) + end + end) + + log.trace("Adding fs watcher for ", target_path) + fs_watch.watch_folder(target_path, fs_watch_callback) + end + end +end + +---@param context neotree.sources.filesystem.Context +---@param dir_path string +local dir_complete = function(context, dir_path) + local paths_to_load = context.paths_to_load + local folders = context.folders + + on_directory_loaded(context, dir_path) + + -- check to see if there are more folders to load + local next_path = nil + while #paths_to_load > 0 and not next_path do + next_path = table.remove(paths_to_load) + -- ensure that the path is still valid + local success, result = pcall(uv.fs_stat, next_path) + -- ensure that the result is a directory + if success and result and result.type == "directory" then + -- ensure that it is not already loaded + local existing = folders[next_path] + if existing and existing.loaded then + next_path = nil + end + else + -- if the path doesn't exist, skip it + next_path = nil + end + end + return next_path +end + +---@param context neotree.sources.filesystem.Context +local render_context = function(context) + local state = context.state + local root = context.root + local parent_id = context.parent_id + + if not parent_id and state.use_libuv_file_watcher and state.enable_git_status then + log.trace("Starting .git folder watcher") + local path = root.path + if root.is_link then + path = root.link_to + end + fs_watch.watch_git_index(path, require("neo-tree").config.git_status_async) + end + fs_watch.updated_watched() + + if root and root.children then + file_items.advanced_sort(root.children, state) + end + if parent_id then + -- lazy loading a child folder + renderer.show_nodes(root.children, state, parent_id, context.callback) + else + -- full render of the tree + renderer.show_nodes({ root }, state, nil, context.callback) + end + + context.state = nil + context.callback = nil + context.all_items = nil + context.root = nil + context.parent_id = nil + ---@diagnostic disable-next-line: cast-local-type + context = nil +end + +---@param context neotree.sources.filesystem.Context +local should_check_gitignore = function(context) + local state = context.state + if #context.all_items == 0 then + log.info("No items, skipping git ignored/status lookups") + return false + end + if state.search_pattern and state.check_gitignore_in_search == false then + return false + end + if state.filtered_items.hide_gitignored then + return true + end + if state.enable_git_status == false then + return false + end + return true +end + +local job_complete_async = function(context) + local state = context.state + local parent_id = context.parent_id + + file_nesting.nest_items(context) + + -- if state.search_pattern and #context.all_items > 50 then + -- -- don't do git ignored/status lookups when searching unless we are down to a reasonable number of items + -- return context + -- end + if should_check_gitignore(context) then + local mark_ignored_async = async.wrap(function(_state, _all_items, _callback) + git.mark_ignored(_state, _all_items, _callback) + end, 3) + local all_items = mark_ignored_async(state, context.all_items) + + if parent_id then + vim.list_extend(state.git_ignored, all_items) + else + state.git_ignored = all_items + end + end + return context +end + +local job_complete = function(context) + local state = context.state + local parent_id = context.parent_id + + file_nesting.nest_items(context) + + if should_check_gitignore(context) then + if require("neo-tree").config.git_status_async then + git.mark_ignored(state, context.all_items, function(all_items) + if parent_id then + vim.list_extend(state.git_ignored, all_items) + else + state.git_ignored = all_items + end + vim.schedule(function() + render_context(context) + end) + end) + return + else + local all_items = git.mark_ignored(state, context.all_items) + if parent_id then + vim.list_extend(state.git_ignored, all_items) + else + state.git_ignored = all_items + end + end + render_context(context) + else + render_context(context) + end +end + +local function create_node(context, node) + pcall(file_items.create_item, context, node.path, node.type) +end + +local function process_node(context, path) + on_directory_loaded(context, path) +end + +---@param err string libuv error +---@return boolean is_permission_error +local function is_permission_error(err) + -- Permission errors may be common when scanning over lots of folders; + -- this is used to check for them and log to `debug` instead of `error`. + return vim.startswith(err, "EPERM") or vim.startswith(err, "EACCES") +end + +local function get_children_sync(path) + local children = {} + local dir, err = uv.fs_opendir(path, nil, ENTRIES_BATCH_SIZE) + if not dir then + ---@cast err -nil + if is_permission_error(err) then + log.debug(err) + else + log.error(err) + end + return children + end + repeat + local stats = uv.fs_readdir(dir) + if not stats then + break + end + local more = false + for i, stat in ipairs(stats) do + more = i == ENTRIES_BATCH_SIZE + local child_path = utils.path_join(path, stat.name) + table.insert(children, { path = child_path, type = stat.type }) + end + until not more + uv.fs_closedir(dir) + return children +end + +local function get_children_async(path, callback) + local children = {} + uv.fs_opendir(path, function(err, dir) + if err then + if is_permission_error(err) then + log.debug(err) + else + log.error(err) + end + callback(children) + return + end + local readdir_batch + ---@param _ string? + ---@param stats uv.fs_readdir.entry[] + readdir_batch = function(_, stats) + if stats then + local more = false + for i, stat in ipairs(stats) do + more = i == ENTRIES_BATCH_SIZE + local child_path = utils.path_join(path, stat.name) + table.insert(children, { path = child_path, type = stat.type }) + end + if more then + return uv.fs_readdir(dir, readdir_batch) + end + end + uv.fs_closedir(dir) + callback(children) + end + + uv.fs_readdir(dir, readdir_batch) + end, ENTRIES_BATCH_SIZE) +end + +local function scan_dir_sync(context, path) + process_node(context, path) + local children = get_children_sync(path) + for _, child in ipairs(children) do + create_node(context, child) + if child.type == "directory" then + local grandchild_nodes = get_children_sync(child.path) + if + grandchild_nodes == nil + or #grandchild_nodes == 0 + or (#grandchild_nodes == 1 and grandchild_nodes[1].type == "directory") + or context.recursive + then + scan_dir_sync(context, child.path) + end + end + end +end + +--- async method +local function scan_dir_async(context, path) + log.debug("scan_dir_async - start " .. path) + + local get_children = async.wrap(function(_path, callback) + return get_children_async(_path, callback) + end, 2) + + local children = get_children(path) + for _, child in ipairs(children) do + create_node(context, child) + if child.type == "directory" then + local grandchild_nodes = get_children(child.path) + if + grandchild_nodes == nil + or #grandchild_nodes == 0 + or (#grandchild_nodes == 1 and grandchild_nodes[1].type == "directory") + or context.recursive + then + scan_dir_async(context, child.path) + end + end + end + + process_node(context, path) + log.debug("scan_dir_async - finish " .. path) + return path +end + +-- async_scan scans all the directories in context.paths_to_load +-- and adds them as items to render in the UI. +---@param context neotree.sources.filesystem.Context +local function async_scan(context, path) + log.trace("async_scan: ", path) + local scan_mode = require("neo-tree").config.filesystem.scan_mode + + if scan_mode == "deep" then + local scan_tasks = {} + for _, p in ipairs(context.paths_to_load) do + local scan_task = function() + scan_dir_async(context, p) + end + table.insert(scan_tasks, scan_task) + end + + async.util.run_all( + scan_tasks, + vim.schedule_wrap(function() + job_complete(context) + end) + ) + return + end + + -- scan_mode == "shallow" + context.directories_scanned = 0 + context.directories_to_scan = #context.paths_to_load + + context.on_exit = vim.schedule_wrap(function() + job_complete(context) + end) + + -- from https://github.com/nvim-lua/plenary.nvim/blob/master/lua/plenary/scandir.lua + local function read_dir(current_dir, ctx) + uv.fs_opendir(current_dir, function(err, dir) + if err then + log.error(current_dir, ": ", err) + return + end + local function on_fs_readdir(err, entries) + if err then + log.error(current_dir, ": ", err) + return + end + if entries then + for _, entry in ipairs(entries) do + local success, item = pcall( + file_items.create_item, + ctx, + utils.path_join(current_dir, entry.name), + entry.type + ) + if success then + if ctx.recursive and item.type == "directory" then + ctx.directories_to_scan = ctx.directories_to_scan + 1 + table.insert(ctx.paths_to_load, item.path) + end + else + log.error("error creating item for ", path) + end + end + + uv.fs_readdir(dir, on_fs_readdir) + return + end + uv.fs_closedir(dir) + on_directory_loaded(ctx, current_dir) + ctx.directories_scanned = ctx.directories_scanned + 1 + if ctx.directories_scanned == #ctx.paths_to_load then + ctx.on_exit() + end + + --local next_path = dir_complete(ctx, current_dir) + --if next_path then + -- local success, error = pcall(read_dir, next_path) + -- if not success then + -- log.error(next_path, ": ", error) + -- end + --else + -- on_exit() + --end + end + + uv.fs_readdir(dir, on_fs_readdir) + end) + end + + --local first = table.remove(context.paths_to_load) + --local success, err = pcall(read_dir, first) + --if not success then + -- log.error(first, ": ", err) + --end + for i = 1, context.directories_to_scan do + read_dir(context.paths_to_load[i], context) + end +end + +---@param context neotree.sources.filesystem.Context +---@param path_to_scan string +local function sync_scan(context, path_to_scan) + log.trace("sync_scan: ", path_to_scan) + local scan_mode = require("neo-tree").config.filesystem.scan_mode + if scan_mode == "deep" then + for _, path in ipairs(context.paths_to_load) do + scan_dir_sync(context, path) + -- scan_dir(context, path) + end + job_complete(context) + else -- scan_mode == "shallow" + local dir, err = uv.fs_opendir(path_to_scan, nil, ENTRIES_BATCH_SIZE) + if dir then + repeat + local stats = uv.fs_readdir(dir) + if not stats then + break + end + + local more = false + for i, stat in ipairs(stats) do + more = i == ENTRIES_BATCH_SIZE + local path = utils.path_join(path_to_scan, stat.name) + local success, _ = pcall(file_items.create_item, context, path, stat.type) + if success then + if context.recursive and stat.type == "directory" then + table.insert(context.paths_to_load, path) + end + else + log.error("error creating item for ", path) + end + end + until not more + uv.fs_closedir(dir) + else + log.error("Error opening dir:", err) + end + + local next_path = dir_complete(context, path_to_scan) + if next_path then + sync_scan(context, next_path) + else + job_complete(context) + end + end +end + +---@param state neotree.sources.filesystem.State +---@param parent_id string? +---@param path_to_reveal string? +---@param callback function +M.get_items_sync = function(state, parent_id, path_to_reveal, callback) + M.get_items(state, parent_id, path_to_reveal, callback, false) +end + +---@param state neotree.sources.filesystem.State +---@param parent_id string? +---@param path_to_reveal string? +---@param callback function +M.get_items_async = function(state, parent_id, path_to_reveal, callback) + M.get_items(state, parent_id, path_to_reveal, callback, true) +end + +---@param context neotree.sources.filesystem.Context +local handle_search_pattern = function(context) + local state = context.state + local root = context.root + local search_opts = { + filtered_items = state.filtered_items, + find_command = state.find_command, + limit = state.search_limit or 50, + path = root.path, + term = state.search_pattern, + find_args = state.find_args, + find_by_full_path_words = state.find_by_full_path_words, + fuzzy_finder_mode = state.fuzzy_finder_mode, + on_insert = function(err, path) + if err then + log.debug(err) + else + file_items.create_item(context, path) + end + end, + on_exit = vim.schedule_wrap(function() + job_complete(context) + end), + } + if state.use_fzy then + filter_external.fzy_sort_files(search_opts, state) + else + -- Use the external command because the plenary search is slow + filter_external.find_files(search_opts) + end +end + +---@param context neotree.sources.filesystem.Context +---@param async_dir_scan boolean +local handle_refresh_or_up = function(context, async_dir_scan) + local parent_id = context.parent_id + local path_to_reveal = context.path_to_reveal + local state = context.state + local path = parent_id or state.path + context.paths_to_load = {} + if parent_id == nil then + if utils.truthy(state.force_open_folders) then + for _, f in ipairs(state.force_open_folders) do + table.insert(context.paths_to_load, f) + end + elseif state.tree then + context.paths_to_load = renderer.get_expanded_nodes(state.tree, state.path) + end + -- Ensure parents of all expanded nodes are also scanned + if #context.paths_to_load > 0 and state.tree then + ---@type table + local seen = {} + for _, p in ipairs(context.paths_to_load) do + ---@type string? + local current = p + while current do + if seen[current] then + break + end + seen[current] = true + local current_node = state.tree:get_node(current) + current = current_node and current_node:get_parent_id() + end + end + context.paths_to_load = vim.tbl_keys(seen) + end + -- Ensure that there are no nested files in the list of folders to load + context.paths_to_load = vim.tbl_filter(function(p) + local stats = uv.fs_stat(p) + return stats and stats.type == "directory" or false + end, context.paths_to_load) + if path_to_reveal then + -- be sure to load all of the folders leading up to the path to reveal + local path_to_reveal_parts = utils.split(path_to_reveal, utils.path_separator) + table.remove(path_to_reveal_parts) -- remove the file name + -- add all parent folders to the list of paths to load + utils.reduce(path_to_reveal_parts, "", function(acc, part) + local current_path = utils.path_join(acc, part) + if #current_path > #path then -- within current root + table.insert(context.paths_to_load, current_path) + table.insert(state.default_expanded_nodes, current_path) + end + return current_path + end) + context.paths_to_load = utils.unique(context.paths_to_load) + end + end + + local filtered_items = state.filtered_items or {} + context.is_a_never_show_file = function(fname) + if fname then + local _, name = utils.split_path(fname) + if name then + if filtered_items.never_show and filtered_items.never_show[name] then + return true + end + if utils.is_filtered_by_pattern(filtered_items.never_show_by_pattern, fname, name) then + return true + end + end + end + return false + end + table.insert(context.paths_to_load, path) + if async_dir_scan then + async_scan(context, path) + else + sync_scan(context, path) + end +end + +---@class neotree.sources.filesystem.Context : neotree.FileItemContext +---@field state neotree.sources.filesystem.State +---@field recursive boolean? +---@field parent_id string? +---@field callback function? +---@field async boolean? +---@field root neotree.FileItem.Directory|neotree.FileItem.Link +---@field directories_scanned integer? +---@field directories_to_scan integer? +---@field on_exit function? +---async +---@field paths_to_load string[] +---@field is_a_never_show_file fun(filename: string?):boolean + +---@class neotree.sources.filesystem.State : neotree.StateWithTree, neotree.Config.Filesystem +---@field path string + +---@param state neotree.sources.filesystem.State +---@param parent_id string? +---@param callback function? +---@param async_dir_scan boolean? +---@param recursive boolean? +M.get_items = function(state, parent_id, path_to_reveal, callback, async_dir_scan, recursive) + renderer.acquire_window(state) + if state.async_directory_scan == "always" then + async_dir_scan = true + elseif state.async_directory_scan == "never" then + async_dir_scan = false + elseif type(async_dir_scan) == "nil" then + async_dir_scan = (state.async_directory_scan == "auto") or state.async_directory_scan ~= nil + end + + if not parent_id then + M.stop_watchers(state) + end + ---@type neotree.sources.filesystem.Context + local context = file_items.create_context() --[[@as neotree.sources.filesystem.Context]] + context.state = state + context.parent_id = parent_id + context.path_to_reveal = path_to_reveal + context.recursive = recursive + context.callback = callback + -- Create root folder + local root = file_items.create_item(context, parent_id or state.path, "directory") --[[@as neotree.FileItem.Directory]] + root.name = vim.fn.fnamemodify(root.path, ":~") + root.loaded = true + root.search_pattern = state.search_pattern + context.root = root + context.folders[root.path] = root + state.default_expanded_nodes = state.force_open_folders or { state.path } + + if state.search_pattern then + handle_search_pattern(context) + else + -- In the case of a refresh or navigating up, we need to make sure that all + -- open folders are loaded. + handle_refresh_or_up(context, async_dir_scan) + end +end + +---@param state neotree.sources.filesystem.State +---@param parent_id string +---@param recursive boolean? +M.get_dir_items_async = function(state, parent_id, recursive) + local context = file_items.create_context() --[[@as neotree.sources.filesystem.Context]] + context.state = state + context.parent_id = parent_id + context.path_to_reveal = nil + context.recursive = recursive + context.callback = nil + context.paths_to_load = {} + + -- Create root folder + local root = file_items.create_item(context, parent_id or state.path, "directory") --[[@as neotree.FileItem.Directory]] + root.name = vim.fn.fnamemodify(root.path, ":~") + root.loaded = true + root.search_pattern = state.search_pattern + context.root = root + context.folders[root.path] = root + state.default_expanded_nodes = state.force_open_folders or { state.path } + + local filtered_items = state.filtered_items or {} + context.is_a_never_show_file = function(fname) + if fname then + local _, name = utils.split_path(fname) + if name then + if filtered_items.never_show and filtered_items.never_show[name] then + return true + end + if utils.is_filtered_by_pattern(filtered_items.never_show_by_pattern, fname, name) then + return true + end + end + end + return false + end + table.insert(context.paths_to_load, parent_id) + + local scan_tasks = {} + for _, p in ipairs(context.paths_to_load) do + local scan_task = function() + scan_dir_async(context, p) + end + table.insert(scan_tasks, scan_task) + end + async.util.join(scan_tasks) + + job_complete_async(context) + + local finalize = async.wrap(function(_context, _callback) + vim.schedule(function() + render_context(_context) + _callback() + end) + end, 2) + finalize(context) +end + +---@param state neotree.sources.filesystem.State +M.stop_watchers = function(state) + if state.use_libuv_file_watcher and state.tree then + -- We are loaded a new root or refreshing, unwatch any folders that were + -- previously being watched. + local loaded_folders = renderer.select_nodes(state.tree, function(node) + return node.type == "directory" and node.loaded + end) + fs_watch.unwatch_git_index(state.path, require("neo-tree").config.git_status_async) + for _, folder in ipairs(loaded_folders) do + log.trace("Unwatching folder ", folder.path) + if folder.is_link then + fs_watch.unwatch_folder(folder.link_to) + else + fs_watch.unwatch_folder(folder:get_id()) + end + end + else + log.debug( + "Not unwatching folders... use_libuv_file_watcher is ", + state.use_libuv_file_watcher, + " and state.tree is ", + utils.truthy(state.tree) + ) + end +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/fs_watch.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/fs_watch.lua new file mode 100644 index 0000000..17424fc --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/fs_watch.lua @@ -0,0 +1,177 @@ +local events = require("neo-tree.events") +local log = require("neo-tree.log") +local git = require("neo-tree.git") +local utils = require("neo-tree.utils") +local uv = vim.uv or vim.loop + +local M = {} + +local flags = { + watch_entry = false, + stat = false, + recursive = false, +} + +local watched = {} + +local get_dot_git_folder = function(path, callback) + if type(callback) == "function" then + git.get_repository_root(path, function(git_root) + if git_root then + local git_folder = utils.path_join(git_root, ".git") + local stat = uv.fs_stat(git_folder) + if stat and stat.type == "directory" then + callback(git_folder, git_root) + end + else + callback(nil, nil) + end + end) + else + local git_root = git.get_repository_root(path) + if git_root then + local git_folder = utils.path_join(git_root, ".git") + local stat = uv.fs_stat(git_folder) + if stat and stat.type == "directory" then + return git_folder, git_root + end + end + return nil, nil + end +end + +M.show_watched = function() + local items = {} + for _, handle in pairs(watched) do + items[handle.path] = handle.references + end + log.info("Watched Folders: ", vim.inspect(items)) +end + +---Watch a directory for changes to it's children. Not recursive. +---@param path string The directory to watch. +---@param custom_callback? function The callback to call when a change is detected. +---@param allow_git_watch? boolean Allow watching of git folders. +M.watch_folder = function(path, custom_callback, allow_git_watch) + if not allow_git_watch then + if path:find("/%.git$") or path:find("/%.git/") then + -- git folders seem to throw off fs events constantly. + log.debug("watch_folder(path): Skipping git folder: ", path) + return + end + end + local h = watched[path] + if h == nil then + log.trace("Starting new fs watch on: ", path) + local callback = custom_callback + or vim.schedule_wrap(function(err, fname) + if fname and fname:match("^%.null[-]ls_.+") then + -- null-ls temp file: https://github.com/jose-elias-alvarez/null-ls.nvim/pull/1075 + return + end + if err then + log.error("file_event_callback: ", err) + return + end + events.fire_event(events.FS_EVENT, { afile = path }) + end) + h = { + handle = uv.new_fs_event(), + path = path, + references = 0, + active = false, + callback = callback, + } + watched[path] = h + --w:start(path, flags, callback) + else + log.trace("Incrementing references for fs watch on: ", path) + end + h.references = h.references + 1 +end + +M.watch_git_index = function(path, async) + local function watch_git_folder(git_folder, git_root) + if git_folder then + local git_event_callback = vim.schedule_wrap(function(err, fname) + if fname and fname:match("^.+%.lock$") then + return + end + if fname and fname:match("^%._null-ls_.+") then + -- null-ls temp file: https://github.com/jose-elias-alvarez/null-ls.nvim/pull/1075 + return + end + if err then + log.error("git_event_callback: ", err) + return + end + events.fire_event(events.GIT_EVENT, { path = fname, repository = git_root }) + end) + + M.watch_folder(git_folder, git_event_callback, true) + end + end + + if async then + get_dot_git_folder(path, watch_git_folder) + else + watch_git_folder(get_dot_git_folder(path)) + end +end + +M.updated_watched = function() + for path, w in pairs(watched) do + if w.references > 0 then + if not w.active then + log.trace("References added for fs watch on: ", path, ", starting.") + w.handle:start(path, flags, w.callback) + w.active = true + end + else + if w.active then + log.trace("No more references for fs watch on: ", path, ", stopping.") + w.handle:stop() + w.active = false + end + end + end +end + +---Stop watching a directory. If there are no more references to the handle, +---it will be destroyed. Otherwise, the reference count will be decremented. +---@param path string The directory to stop watching. +M.unwatch_folder = function(path, callback_id) + local h = watched[path] + if h then + log.trace("Decrementing references for fs watch on: ", path, callback_id) + h.references = h.references - 1 + else + log.trace("(unwatch_folder) No fs watch found for: ", path) + end +end + +M.unwatch_git_index = function(path, async) + local function unwatch_git_folder(git_folder, _) + if git_folder then + M.unwatch_folder(git_folder) + end + end + + if async then + get_dot_git_folder(path, unwatch_git_folder) + else + unwatch_git_folder(get_dot_git_folder(path)) + end +end + +---Stop watching all directories. This is the nuclear option and it affects all +---sources. +M.unwatch_all = function() + for _, h in pairs(watched) do + h.handle:stop() + h.handle = nil + end + watched = {} +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/globtopattern.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/globtopattern.lua new file mode 100644 index 0000000..8f94985 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/filesystem/lib/globtopattern.lua @@ -0,0 +1,157 @@ +--(c) 2008-2011 David Manura. Licensed under the same terms as Lua (MIT). + +--Permission is hereby granted, free of charge, to any person obtaining a copy +--of this software and associated documentation files (the "Software"), to deal +--in the Software without restriction, including without limitation the rights +--to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +--copies of the Software, and to permit persons to whom the Software is +--furnished to do so, subject to the following conditions: + +--The above copyright notice and this permission notice shall be included in +--all copies or substantial portions of the Software. + +--THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +--IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +--FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +--AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +--LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +--OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +--THE SOFTWARE. +--(end license) + +local M = { _TYPE = "module", _NAME = "globtopattern", _VERSION = "0.2.1.20120406" } + +function M.globtopattern(g) + -- Some useful references: + -- - apr_fnmatch in Apache APR. For example, + -- http://apr.apache.org/docs/apr/1.3/group__apr__fnmatch.html + -- which cites POSIX 1003.2-1992, section B.6. + + local p = "^" -- pattern being built + local i = 0 -- index in g + local c -- char at index i in g. + + -- unescape glob char + local function unescape() + if c == "\\" then + i = i + 1 + c = g:sub(i, i) + if c == "" then + p = "[^]" + return false + end + end + return true + end + + -- escape pattern char + local function escape(c) + return c:match("^%w$") and c or "%" .. c + end + + -- Convert tokens at end of charset. + local function charset_end() + while 1 do + if c == "" then + p = "[^]" + return false + elseif c == "]" then + p = p .. "]" + break + else + if not unescape() then + break + end + local c1 = c + i = i + 1 + c = g:sub(i, i) + if c == "" then + p = "[^]" + return false + elseif c == "-" then + i = i + 1 + c = g:sub(i, i) + if c == "" then + p = "[^]" + return false + elseif c == "]" then + p = p .. escape(c1) .. "%-]" + break + else + if not unescape() then + break + end + p = p .. escape(c1) .. "-" .. escape(c) + end + elseif c == "]" then + p = p .. escape(c1) .. "]" + break + else + p = p .. escape(c1) + i = i - 1 -- put back + end + end + i = i + 1 + c = g:sub(i, i) + end + return true + end + + -- Convert tokens in charset. + local function charset() + i = i + 1 + c = g:sub(i, i) + if c == "" or c == "]" then + p = "[^]" + return false + elseif c == "^" or c == "!" then + i = i + 1 + c = g:sub(i, i) + if c == "]" then + -- ignored + else + p = p .. "[^" + if not charset_end() then + return false + end + end + else + p = p .. "[" + if not charset_end() then + return false + end + end + return true + end + + -- Convert tokens. + while 1 do + i = i + 1 + c = g:sub(i, i) + if c == "" then + p = p .. "$" + break + elseif c == "?" then + p = p .. "." + elseif c == "*" then + p = p .. ".*" + elseif c == "[" then + if not charset() then + break + end + elseif c == "\\" then + i = i + 1 + c = g:sub(i, i) + if c == "" then + p = p .. "\\$" + break + end + p = p .. escape(c) + else + p = p .. escape(c) + end + end + return p +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/git_status/commands.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/git_status/commands.lua new file mode 100644 index 0000000..d7bf5b6 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/git_status/commands.lua @@ -0,0 +1,74 @@ +--This file should contain all commands meant to be used by mappings. + +local cc = require("neo-tree.sources.common.commands") +local utils = require("neo-tree.utils") +local manager = require("neo-tree.sources.manager") + +---@class neotree.sources.GitStatus.Commands : neotree.sources.Common.Commands +local M = {} + +local refresh = utils.wrap(manager.refresh, "git_status") +local redraw = utils.wrap(manager.redraw, "git_status") + +-- ---------------------------------------------------------------------------- +-- Common commands +-- ---------------------------------------------------------------------------- +M.add = function(state) + cc.add(state, refresh) +end + +M.add_directory = function(state) + cc.add_directory(state, refresh) +end + +---Marks node as copied, so that it can be pasted somewhere else. +M.copy_to_clipboard = function(state) + cc.copy_to_clipboard(state, redraw) +end + +---@type neotree.TreeCommandVisual +M.copy_to_clipboard_visual = function(state, selected_nodes) + cc.copy_to_clipboard_visual(state, selected_nodes, redraw) +end + +---Marks node as cut, so that it can be pasted (moved) somewhere else. +M.cut_to_clipboard = function(state) + cc.cut_to_clipboard(state, redraw) +end + +---@type neotree.TreeCommandVisual +M.cut_to_clipboard_visual = function(state, selected_nodes) + cc.cut_to_clipboard_visual(state, selected_nodes, redraw) +end + +M.copy = function(state) + cc.copy(state, redraw) +end + +M.move = function(state) + cc.move(state, redraw) +end + +---Pastes all items from the clipboard to the current directory. +M.paste_from_clipboard = function(state) + cc.paste_from_clipboard(state, refresh) +end + +M.delete = function(state) + cc.delete(state, refresh) +end + +---@type neotree.TreeCommandVisual +M.delete_visual = function(state, selected_nodes) + cc.delete_visual(state, selected_nodes, refresh) +end + +M.refresh = refresh + +M.rename = function(state) + cc.rename(state, refresh) +end + +cc._add_common_commands(M) + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/git_status/components.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/git_status/components.lua new file mode 100644 index 0000000..4565ad7 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/git_status/components.lua @@ -0,0 +1,56 @@ +-- This file contains the built-in components. Each componment is a function +-- that takes the following arguments: +-- config: A table containing the configuration provided by the user +-- when declaring this component in their renderer config. +-- node: A NuiNode object for the currently focused node. +-- state: The current state of the source providing the items. +-- +-- The function should return either a table, or a list of tables, each of which +-- contains the following keys: +-- text: The text to display for this item. +-- highlight: The highlight group to apply to this text. + +local highlights = require("neo-tree.ui.highlights") +local common = require("neo-tree.sources.common.components") + +---@alias neotree.Component.GitStatus._Key +---|"name" + +---@class neotree.Component.GitStatus +---@field [1] neotree.Component.GitStatus._Key|neotree.Component.Common._Key + +---@type table +local M = {} + +---@class (exact) neotree.Component.GitStatus.Name : neotree.Component.Common.Name +---@field [1] "current_filter"? +---@field use_git_status_colors boolean? + +---@param config neotree.Component.GitStatus.Name +M.name = function(config, node, state) + local highlight = config.highlight or highlights.FILE_NAME_OPENED + local name = node.name + if node.type == "directory" then + if node:get_depth() == 1 then + highlight = highlights.ROOT_NAME + if node:has_children() then + name = "GIT STATUS for " .. name + else + name = "GIT STATUS (working tree clean) for " .. name + end + else + highlight = highlights.DIRECTORY_NAME + end + elseif config.use_git_status_colors then + local git_status = state.components.git_status({}, node, state) + if git_status and git_status.highlight then + highlight = git_status.highlight + end + end + return { + text = name, + highlight = highlight, + } +end + +return vim.tbl_deep_extend("force", common, M) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/git_status/init.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/git_status/init.lua new file mode 100644 index 0000000..cfa540c --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/git_status/init.lua @@ -0,0 +1,111 @@ +--This file should have all functions that are in the public api and either set +--or read the state of this source. + +local utils = require("neo-tree.utils") +local renderer = require("neo-tree.ui.renderer") +local items = require("neo-tree.sources.git_status.lib.items") +local events = require("neo-tree.events") +local manager = require("neo-tree.sources.manager") + +---@class neotree.sources.GitStatus : neotree.Source +local M = { + name = "git_status", + display_name = " 󰊢 Git ", +} + +local wrap = function(func) + return utils.wrap(func, M.name) +end + +local get_state = function() + return manager.get_state(M.name) +end + +---Navigate to the given path. +---@param path string Path to navigate to. If empty, will navigate to the cwd. +M.navigate = function(state, path, path_to_reveal, callback, async) + state.path = path or state.path + state.dirty = false + if path_to_reveal then + renderer.position.set(state, path_to_reveal) + end + items.get_git_status(state) + + if type(callback) == "function" then + vim.schedule(callback) + end +end + +M.refresh = function() + manager.refresh(M.name) +end + +---@class neotree.Config.GitStatus.Renderers : neotree.Config.Renderers + +---@class (exact) neotree.Config.GitStatus : neotree.Config.Source +---@field bind_to_cwd boolean? +---@field renderers neotree.Config.GitStatus.Renderers? + +---Configures the plugin, should be called before the plugin is used. +---@param config neotree.Config.GitStatus Configuration table containing any keys that the user +--wants to change from the defaults. May be empty to accept default values. +M.setup = function(config, global_config) + if config.before_render then + --convert to new event system + manager.subscribe(M.name, { + event = events.BEFORE_RENDER, + handler = function(state) + local this_state = get_state() + if state == this_state then + config.before_render(this_state) + end + end, + }) + end + + if global_config.enable_refresh_on_write then + manager.subscribe(M.name, { + event = events.VIM_BUFFER_CHANGED, + handler = function(args) + if utils.is_real_file(args.afile) then + M.refresh() + end + end, + }) + end + + if config.bind_to_cwd then + manager.subscribe(M.name, { + event = events.VIM_DIR_CHANGED, + handler = M.refresh, + }) + end + + if global_config.enable_diagnostics then + manager.subscribe(M.name, { + event = events.STATE_CREATED, + handler = function(state) + state.diagnostics_lookup = utils.get_diagnostic_counts() + end, + }) + manager.subscribe(M.name, { + event = events.VIM_DIAGNOSTIC_CHANGED, + handler = wrap(manager.diagnostics_changed), + }) + end + + --Configure event handlers for modified files + if global_config.enable_modified_markers then + manager.subscribe(M.name, { + event = events.VIM_BUFFER_MODIFIED_SET, + handler = wrap(manager.opened_buffers_changed), + }) + end + + manager.subscribe(M.name, { + event = events.GIT_EVENT, + handler = M.refresh, + }) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/git_status/lib/items.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/git_status/lib/items.lua new file mode 100644 index 0000000..a4b7caf --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/git_status/lib/items.lua @@ -0,0 +1,49 @@ +local renderer = require("neo-tree.ui.renderer") +local file_items = require("neo-tree.sources.common.file-items") +local log = require("neo-tree.log") +local git = require("neo-tree.git") + +local M = {} + +---Get a table of all open buffers, along with all parent paths of those buffers. +---The paths are the keys of the table, and all the values are 'true'. +---@param state neotree.State +M.get_git_status = function(state) + if state.loading then + return + end + state.loading = true + local status_lookup, project_root = git.status(state.git_base, true, state.path) + state.path = project_root or state.path or vim.fn.getcwd() + local context = file_items.create_context() + context.state = state + -- Create root folder + local root = file_items.create_item(context, state.path, "directory") --[[@as neotree.FileItem.Directory]] + root.name = vim.fn.fnamemodify(root.path, ":~") + root.loaded = true + root.search_pattern = state.search_pattern + context.folders[root.path] = root + + for path, status in pairs(status_lookup) do + local success, item = pcall(file_items.create_item, context, path, "file") --[[@as neotree.FileItem.File]] + item.status = status + if success then + item.extra = { + git_status = status, + } + else + log.error("Error creating item for " .. path .. ": " .. item) + end + end + + state.git_status_lookup = status_lookup + state.default_expanded_nodes = {} + for id, _ in pairs(context.folders) do + table.insert(state.default_expanded_nodes, id) + end + file_items.advanced_sort(root.children, state) + renderer.show_nodes({ root }, state) + state.loading = false +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/manager.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/manager.lua new file mode 100644 index 0000000..92f0c8a --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/sources/manager.lua @@ -0,0 +1,772 @@ +--This file should have all functions that are in the public api and either set +--or read the state of this source. +local nt = require("neo-tree") +local utils = require("neo-tree.utils") +local compat = require("neo-tree.utils._compat") +local fs_scan = require("neo-tree.sources.filesystem.lib.fs_scan") +local renderer = require("neo-tree.ui.renderer") +local inputs = require("neo-tree.ui.inputs") +local events = require("neo-tree.events") +local log = require("neo-tree.log") +local fs_watch = require("neo-tree.sources.filesystem.lib.fs_watch") + +local M = {} +---@type table +local source_data = {} +---@type neotree.State[] +local all_states = {} +---@type table +local default_configs = {} + +---@class neotree.SourceData +---@field name string +---@field state_by_tab table +---@field state_by_win table +---@field subscriptions table +---@field module neotree.Source? + +---@param source_name string +---@return neotree.SourceData +local get_source_data = function(source_name) + assert(source_name, "get_source_data: source_name cannot be nil") + local sd = source_data[source_name] + if sd then + return sd + end + sd = { + name = source_name, + state_by_tab = {}, + state_by_win = {}, + subscriptions = {}, + } + source_data[source_name] = sd + return sd +end + +---@class neotree.State.Window : neotree.Config.Window +---@field win_width integer +---@field last_user_width integer + +---@alias neotree.State.Position "top"|"bottom"|"left"|"right"|"current"|"float" + +---@alias neotree.Internal.SortFieldProvider fun(node: NuiTree.Node):any + +---@class neotree.State : neotree.Config.Source +---@field name string +---@field tabid integer +---@field id integer +---@field bufnr integer? +---@field dirty boolean +---@field position table +---@field git_base string +---@field sort table +---@field clipboard table +---@field current_position neotree.State.Position? +---@field disposed boolean? +---@field winid integer? +---@field path string? +---@field tree NuiTree? +---@field components table +---private-ish +---@field orig_tree NuiTree? +---@field _ready boolean? +---@field loading boolean? +---window +---@field window neotree.State.Window? +---@field win_width integer? +---@field longest_width_exact integer? +---@field longest_node integer? +---extras +---@field bind_to_cwd boolean? +---@field opened_buffers neotree.utils.OpenedBuffers? +---@field diagnostics_lookup neotree.utils.DiagnosticLookup? +---@field cwd_target neotree.Config.Filesystem.CwdTarget? +---@field sort_field_provider fun(node: NuiTree.Node):any +---@field explicitly_opened_nodes table? +---@field filtered_items neotree.Config.Filesystem.FilteredItems? +---@field skip_marker_at_level table? +---@field group_empty_dirs boolean? +---git +---@field git_status_lookup neotree.git.Status? +---optional mapping args +---@field fallback string? +---@field config table? +---internal +---@field default_expanded_nodes NuiTree.Node[]? +---@field force_open_folders string[]? +---@field enable_source_selector boolean? +---@field follow_current_file neotree.Config.Filesystem.FollowCurrentFile? +---lsp +---@field lsp_winid number? +---@field lsp_bufnr number? +---search +---@field search_pattern string? +---@field use_fzy boolean? +---@field fzy_sort_result_scores table? +---@field fuzzy_finder_mode "directory"|boolean? +---@field open_folders_before_search table? +---sort +---@field sort_function_override neotree.Config.SortFunction? +---keymaps +---@field resolved_mappings table? +---@field commands table? + +---@class (exact) neotree.StateWithTree : neotree.State +---@field tree NuiTree + +local a = {} + +---@param tabid integer +---@param sd table +---@param winid integer? +---@return neotree.State +local function create_state(tabid, sd, winid) + nt.ensure_config() + local default_config = assert(default_configs[sd.name]) + local state = vim.deepcopy(default_config, compat.noref()) + ---@cast state neotree.State + state.tabid = tabid + state.id = winid or tabid + state.dirty = true + state.position = {} + state.git_base = "HEAD" + state.sort = { label = "Name", direction = 1 } + events.fire_event(events.STATE_CREATED, state) + table.insert(all_states, state) + return state +end + +M._get_all_states = function() + return all_states +end + +---@param source_name string? +---@param action fun(state: neotree.State) +M._for_each_state = function(source_name, action) + M.dispose_invalid_tabs() + for _, state in ipairs(all_states) do + if source_name == nil or state.name == source_name then + action(state) + end + end +end + +---For use in tests only, completely resets the state of all sources. +---This closes all windows as well since they would be broken by this action. +M._clear_state = function() + fs_watch.unwatch_all() + renderer.close_all_floating_windows() + for _, data in pairs(source_data) do + for _, state in pairs(data.state_by_tab) do + renderer.close(state) + end + for _, state in pairs(data.state_by_win) do + renderer.close(state) + end + end + source_data = {} +end + +---@param source_name string +---@param config neotree.Config.Source +M.set_default_config = function(source_name, config) + if source_name == nil then + error("set_default_config: source_name cannot be nil") + end + default_configs[source_name] = config + local sd = get_source_data(source_name) + for tabid, tab_config in pairs(sd.state_by_tab) do + sd.state_by_tab[tabid] = vim.tbl_deep_extend("force", tab_config, config) + end +end + +--TODO: we need to track state per window when working with netwrw style "current" +--position. How do we know which one to return when this is called? +---@param source_name string +---@param tabid integer? +---@param winid integer? +---@return neotree.State +M.get_state = function(source_name, tabid, winid) + assert(source_name, "get_state: source_name cannot be nil") + tabid = tabid or vim.api.nvim_get_current_tabpage() + local sd = get_source_data(source_name) + if type(winid) == "number" then + local win_state = sd.state_by_win[winid] + if not win_state then + win_state = create_state(tabid, sd, winid) + sd.state_by_win[winid] = win_state + end + return win_state + end + local tab_state = sd.state_by_tab[tabid] + if tab_state and tab_state.winid then + -- just in case tab and window get tangled up, tab state replaces window + sd.state_by_win[tab_state.winid] = nil + end + if not tab_state then + tab_state = create_state(tabid, sd) + sd.state_by_tab[tabid] = tab_state + end + return tab_state +end + +---Returns the state for the current buffer, assuming it is a neo-tree buffer. +---@param winid number? The window id to use, if nil, the current window is used. +---@return neotree.State? state The state for the current buffer, if it's a neo-tree buffer. +M.get_state_for_window = function(winid) + winid = winid or vim.api.nvim_get_current_win() + local bufnr = vim.api.nvim_win_get_buf(winid) + local source_status, source_name = pcall(vim.api.nvim_buf_get_var, bufnr, "neo_tree_source") + local position_status, position = pcall(vim.api.nvim_buf_get_var, bufnr, "neo_tree_position") + if not source_status or not position_status then + return nil + end + + local tabid = vim.api.nvim_get_current_tabpage() + if position == "current" then + return M.get_state(source_name, tabid, winid) + else + return M.get_state(source_name, tabid, nil) + end +end + +M.get_path_to_reveal = function(include_terminals) + local win_id = vim.api.nvim_get_current_win() + local cfg = vim.api.nvim_win_get_config(win_id) + if cfg.relative > "" or cfg.external then + -- floating window, ignore + return nil + end + if vim.bo.filetype == "neo-tree" then + return nil + end + local path = vim.fn.expand("%:p") + if not utils.truthy(path) then + return nil + end + if not include_terminals and path:match("term://") then + return nil + end + return path +end + +---@param source_name string +M.subscribe = function(source_name, event) + assert(source_name, "subscribe: source_name cannot be nil") + local sd = get_source_data(source_name) + if not sd.subscriptions then + sd.subscriptions = {} + end + if not utils.truthy(event.id) then + event.id = sd.name .. "." .. event.event + end + log.trace("subscribing to event: " .. event.id) + sd.subscriptions[event] = true + events.subscribe(event) +end + +---@param source_name string +M.unsubscribe = function(source_name, event) + assert(source_name, "unsubscribe: source_name cannot be nil") + local sd = get_source_data(source_name) + log.trace("unsubscribing to event: " .. event.id or event.event) + if sd.subscriptions then + for sub, _ in pairs(sd.subscriptions) do + if sub.event == event.event and sub.id == event.id then + sd.subscriptions[sub] = false + events.unsubscribe(sub) + end + end + end + events.unsubscribe(event) +end + +---@param source_name string +M.unsubscribe_all = function(source_name) + assert(source_name, "unsubscribe_all: source_name cannot be nil") + local sd = get_source_data(source_name) + if sd.subscriptions then + for event, subscribed in pairs(sd.subscriptions) do + if subscribed then + events.unsubscribe(event) + end + end + end + sd.subscriptions = {} +end + +---@param source_name string +M.close = function(source_name, at_position) + local state = M.get_state(source_name) + if at_position then + if state.current_position == at_position then + return renderer.close(state) + else + return false + end + else + return renderer.close(state) + end +end + +M.close_all = function(at_position) + local tabid = vim.api.nvim_get_current_tabpage() + for source_name, _ in pairs(source_data) do + M._for_each_state(source_name, function(state) + if state.tabid == tabid then + if at_position then + if state.current_position == at_position then + log.trace("Closing " .. source_name .. " at position " .. at_position) + pcall(renderer.close, state) + end + else + log.trace("Closing " .. source_name) + pcall(renderer.close, state) + end + end + end) + end +end + +M.close_all_except = function(except_source_name) + local tabid = vim.api.nvim_get_current_tabpage() + for source_name, _ in pairs(source_data) do + M._for_each_state(source_name, function(state) + if state.tabid == tabid and source_name ~= except_source_name then + log.trace("Closing " .. source_name) + pcall(renderer.close, state) + end + end) + end +end + +---Redraws the tree with updated diagnostics without scanning the filesystem again. +---@param source_name string +---@param args table +M.diagnostics_changed = function(source_name, args) + if not type(args) == "table" then + error("diagnostics_changed: args must be a table") + end + M._for_each_state(source_name, function(state) + state.diagnostics_lookup = args.diagnostics_lookup + renderer.redraw(state) + end) +end + +---Called by autocmds when the cwd dir is changed. This will change the root. +---@param source_name string +M.dir_changed = function(source_name) + M._for_each_state(source_name, function(state) + local cwd = M.get_cwd(state) + if state.path and cwd == state.path then + return + end + if renderer.window_exists(state) then + M.navigate(state, cwd) + else + state.path = nil + state.dirty = true + end + end) +end +-- +---Redraws the tree with updated git_status without scanning the filesystem again. +---@param source_name string +M.git_status_changed = function(source_name, args) + if not type(args) == "table" then + error("git_status_changed: args must be a table") + end + M._for_each_state(source_name, function(state) + if utils.is_subpath(args.git_root, state.path) then + state.git_status_lookup = args.git_status + renderer.redraw(state) + end + end) +end + +-- Vimscript functions like vim.fn.getcwd take tabpage number (tab position counting from left) +-- but API functions operate on tabpage id (as returned by nvim_tabpage_get_number). These values +-- get out of sync when tabs are being moved and we want to track state according to tabpage id. +local to_tabnr = function(tabid) + return tabid > 0 and vim.api.nvim_tabpage_get_number(tabid) or tabid +end + +---@param state neotree.State +local get_params_for_cwd = function(state) + local tabid = state.tabid + -- the id is either the tabid for sidebars or the winid for splits + local winid = state.id == tabid and -1 or state.id + + if state.cwd_target then + local target = state.cwd_target.sidebar + if state.current_position == "current" then + target = state.cwd_target.current + end + if target == "window" then + return winid, to_tabnr(tabid) + elseif target == "global" then + return -1, -1 + elseif target == "none" then + return nil, nil + else -- default to tab + return -1, to_tabnr(tabid) + end + else + return winid, to_tabnr(tabid) + end +end + +---@param state neotree.State +---@return string +M.get_cwd = function(state) + local winid, tabnr = get_params_for_cwd(state) + if winid or tabnr then + local success, cwd = pcall(vim.fn.getcwd, winid, tabnr) + if success then + return cwd + end + end + + local success, cwd = pcall(vim.fn.getcwd) + if success then + return cwd + end + + local err = cwd + log.debug(err) + return state.path or "" +end + +---@param state neotree.State +M.set_cwd = function(state) + if not state.path then + return + end + + local winid, tabnr = get_params_for_cwd(state) + + if winid == nil and tabnr == nil then + return + end + + local _, cwd = pcall(vim.fn.getcwd, winid, tabnr) + if state.path ~= cwd then + local path = utils.escape_path_for_cmd(state.path) + if winid > 0 then + vim.cmd("lcd " .. path) + elseif tabnr > 0 then + vim.cmd("tcd " .. path) + else + vim.cmd("cd " .. path) + end + end +end + +---@param state neotree.State +local dispose_state = function(state) + pcall(fs_scan.stop_watchers, state) + pcall(renderer.close, state) + source_data[state.name].state_by_tab[state.id] = nil + source_data[state.name].state_by_win[state.id] = nil + state.disposed = true +end + +---@param source_name string +---@param tabid integer +M.dispose = function(source_name, tabid) + -- Iterate in reverse because we are removing items during loop + for i = #all_states, 1, -1 do + local state = all_states[i] + if source_name == nil or state.name == source_name then + if not tabid or tabid == state.tabid then + log.trace(state.name, " disposing of tab: ", tabid) + dispose_state(state) + table.remove(all_states, i) + end + end + end +end + +---@param tabid integer +M.dispose_tab = function(tabid) + if not tabid then + error("dispose_tab: tabid cannot be nil") + end + -- Iterate in reverse because we are removing items during loop + for i = #all_states, 1, -1 do + local state = all_states[i] + if tabid == state.tabid then + log.trace(state.name, " disposing of tab: ", tabid, state.name) + dispose_state(state) + table.remove(all_states, i) + end + end +end + +M.dispose_invalid_tabs = function() + -- Iterate in reverse because we are removing items during loop + for i = #all_states, 1, -1 do + local state = all_states[i] + -- if not valid_tabs[state.tabid] then + if not vim.api.nvim_tabpage_is_valid(state.tabid) then + log.trace(state.name, " disposing of tab: ", state.tabid, state.name) + dispose_state(state) + table.remove(all_states, i) + end + end +end + +---@param winid number +M.dispose_window = function(winid) + assert(winid, "dispose_window: winid cannot be nil") + -- Iterate in reverse because we are removing items during loop + for i = #all_states, 1, -1 do + local state = all_states[i] + if state.id == winid then + log.trace(state.name, " disposing of window: ", winid, state.name) + dispose_state(state) + table.remove(all_states, i) + end + end +end + +---@param source_name string +M.float = function(source_name) + local state = M.get_state(source_name) + state.current_position = "float" + local path_to_reveal = M.get_path_to_reveal() + M.navigate(source_name, state.path, path_to_reveal) +end + +---Focus the window, opening it if it is not already open. +---@param source_name string Source name. +---@param path_to_reveal string|nil Node to focus after the items are loaded. +---@param callback function|nil Callback to call after the items are loaded. +M.focus = function(source_name, path_to_reveal, callback) + local state = M.get_state(source_name) + state.current_position = nil + if path_to_reveal then + M.navigate(source_name, state.path, path_to_reveal, callback) + else + if not state.dirty and renderer.window_exists(state) then + vim.api.nvim_set_current_win(state.winid) + else + M.navigate(source_name, state.path, nil, callback) + end + end +end + +---Redraws the tree with updated modified markers without scanning the filesystem again. +M.opened_buffers_changed = function(source_name, args) + if not type(args) == "table" then + error("opened_buffers_changed: args must be a table") + end + if type(args.opened_buffers) == "table" then + M._for_each_state(source_name, function(state) + if utils.tbl_equals(args.opened_buffers, state.opened_buffers) then + -- no changes, no need to redraw + return + end + state.opened_buffers = args.opened_buffers + renderer.redraw(state) + end) + end +end + +---Navigate to the given path. +---@param state_or_source_name neotree.State|string The state or source name to navigate. +---@param path string? Path to navigate to. If empty, will navigate to the cwd. +---@param path_to_reveal string? Node to focus after the items are loaded. +---@param callback function? Callback to call after the items are loaded. +---@param async boolean? Whether to load the items asynchronously, may not be respected by all sources. +M.navigate = function(state_or_source_name, path, path_to_reveal, callback, async) + require("neo-tree").ensure_config() + local state, source_name + if type(state_or_source_name) == "string" then + state = M.get_state(state_or_source_name) + source_name = state_or_source_name + elseif type(state_or_source_name) == "table" then + state = state_or_source_name + source_name = state.name + else + log.error("navigate: state_or_source_name must be a string or a table") + return + end + log.trace("navigate", source_name, path, path_to_reveal) + local mod = get_source_data(source_name).module + if not mod then + mod = require("neo-tree.sources." .. source_name) + end + mod.navigate(state, path, path_to_reveal, callback, async) +end + +---Redraws the tree without scanning the filesystem again. Use this after +-- making changes to the nodes that would affect how their components are +-- rendered. +M.redraw = function(source_name) + M._for_each_state(source_name, function(state) + renderer.redraw(state) + end) +end + +---Refreshes the tree by scanning the filesystem again. +M.refresh = function(source_name, callback) + if type(callback) ~= "function" then + callback = nil + end + local current_tabid = vim.api.nvim_get_current_tabpage() + log.trace(source_name, "refresh") + for i = 1, #all_states, 1 do + local state = all_states[i] + if state.tabid == current_tabid and state.path and renderer.window_exists(state) then + local success, err = pcall(M.navigate, state, state.path, nil, callback) + if not success then + log.error(err) + end + else + state.dirty = true + end + end +end + +--- @deprecated +--- To be removed in 4.0. Use: +--- ```lua +--- require("neo-tree.command").execute({ source_name = source_name, action = "focus", reveal = true })` instead +--- ``` +M.reveal_current_file = function(source_name, callback, force_cwd) + log.warn( + [[DEPRECATED: use `require("neo-tree.command").execute({ source_name = source_name, action = "focus", reveal = true })` instead]] + ) + + log.trace("Revealing current file") + local state = M.get_state(source_name) + state.current_position = nil + + local path = M.get_path_to_reveal() + if not path then + M.focus(source_name) + return + end + local cwd = state.path + if cwd == nil then + cwd = M.get_cwd(state) + end + if force_cwd then + if not utils.is_subpath(cwd, path) then + state.path, _ = utils.split_path(path) + end + elseif not utils.is_subpath(cwd, path) then + cwd, _ = utils.split_path(path) + inputs.confirm("File not in cwd. Change cwd to " .. cwd .. "?", function(response) + if response == true then + state.path = cwd + M.focus(source_name, path, callback) + else + M.focus(source_name, nil, callback) + end + end) + return + end + if path then + if not renderer.focus_node(state, path) then + M.focus(source_name, path, callback) + end + end +end + +---@deprecated +--- To be removed in 4.0. Use: +--- ```lua +--- require("neo-tree.command").execute({ source_name = source_name, action = "focus", reveal = true, position = "current" } +--- ``` +--- instead. +M.reveal_in_split = function(source_name, callback) + log.warn( + [[DEPRECATED: use `require("neo-tree.command").execute({ source_name = source_name, action = "focus", reveal = true, position = "current" })` instead]] + ) + + local state = M.get_state(source_name, nil, vim.api.nvim_get_current_win()) + state.current_position = "current" + local path_to_reveal = M.get_path_to_reveal() + if not path_to_reveal then + M.navigate(state, nil, nil, callback) + return + end + local cwd = state.path + if cwd == nil then + cwd = M.get_cwd(state) + end + if cwd and not utils.is_subpath(cwd, path_to_reveal) then + state.path, _ = utils.split_path(path_to_reveal) + end + M.navigate(state, state.path, path_to_reveal, callback) +end + +---Opens the tree and displays the current path or cwd, without focusing it. +M.show = function(source_name) + local state = M.get_state(source_name) + state.current_position = nil + if not renderer.window_exists(state) then + local current_win = vim.api.nvim_get_current_win() + M.navigate(source_name, state.path, nil, function() + vim.api.nvim_set_current_win(current_win) + end) + end +end + +M.show_in_split = function(source_name, callback) + local state = M.get_state(source_name, nil, vim.api.nvim_get_current_win()) + state.current_position = "current" + M.navigate(state, state.path, nil, callback) +end + +local validate = require("neo-tree.health.typecheck").validate +---@param source_name string +---@param module neotree.Source +M.validate_source = function(source_name, module) + if source_name == nil then + error("register_source: source_name cannot be nil") + end + if module == nil then + error("register_source: module cannot be nil") + end + if type(module) ~= "table" then + error("register_source: module must be a table") + end + validate(source_name, module, function(mod) + validate("navigate", mod.navigate, "function") + validate("setup", mod.setup, "function") + end) +end + +---@class neotree.Source +---@field setup fun(config: neotree.Config.Source, global_config: neotree.Config.Base) +---@field navigate fun(state: neotree.State, path: string?, path_to_reveal: string?, callback: function?, async: boolean?) + +---Configures the plugin, should be called before the plugin is used. +---@param source_name string Name of the source. +---@param config neotree.Config.Source Configuration table containing merged configuration for the source. +---@param global_config neotree.Config.Base Global configuration table, shared between all sources. +---@param module neotree.Source Module containing the source's code. +M.setup = function(source_name, config, global_config, module) + log.debug(source_name, " setup ", config) + M.unsubscribe_all(source_name) + M.set_default_config(source_name, config) + if module == nil then + module = require("neo-tree.sources." .. source_name) + end + local success, err = pcall(M.validate_source, source_name, module) + if success then + success, err = pcall(module.setup, config, global_config) + if success then + get_source_data(source_name).module = module + else + log.error("Source " .. source_name .. " setup failed: " .. err) + end + else + log.error("Source " .. source_name .. " is invalid: " .. err) + end +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/components.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/components.lua new file mode 100644 index 0000000..b95bb1e --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/components.lua @@ -0,0 +1,16 @@ +---@meta + +---@alias neotree.Renderer fun(config: table, node: NuiTree.Node, state: neotree.StateWithTree):(neotree.Render.Node|neotree.Render.Node[]) + +---@alias neotree.FileRenderer fun(config: table, node: neotree.FileNode, state: neotree.StateWithTree):(neotree.Render.Node|neotree.Render.Node[]) + +---@class (exact) neotree.Render.Node +---@field text string The text to display. +---@field highlight string The highlight for the text. + +---@class (exact) neotree.Component +---@field [1] string? +---@field enabled boolean? +---@field highlight string? + +---@alias neotree.IconProvider fun(icon: neotree.Render.Node, node: NuiTree.Node, state: neotree.StateWithTree):(neotree.Render.Node|neotree.Render.Node[]|nil) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/config.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/config.lua new file mode 100644 index 0000000..cef9f03 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/config.lua @@ -0,0 +1,151 @@ +---@meta + +---@class neotree.Config.Mapping.Options +---@field noremap boolean? +---@field nowait boolean? +---@field desc string? + +---@class neotree.Config.Window.Command.Configured : neotree.Config.Mapping.Options +---@field [1] string? +---@field command string? +---@field config table? + +---@class neotree.Config.Source +---@field window neotree.Config.Window? +---@field renderers neotree.Config.Renderers? +---@field commands table? +---@field before_render fun(state: neotree.State)? + +---@class neotree.Config.SourceSelector.Item +---@field source string? +---@field padding integer|{left:integer,right:integer}? +---@field separator string|{left:string,right:string, override?:string}? + +---@alias neotree.Config.SourceSelector.Separator.Override +---|"right" # When right and left separators meet, only show the right one. +---|"left" # When right and left separators meet, only show the left one. +---|"active" # Only use the left separator on the left of the active tab, and only the right afterwards. +---|nil # Show both separators. + +---@class neotree.Config.SourceSelector.Separator +---@field left string? +---@field right string? +---@field override neotree.Config.SourceSelector.Separator.Override? + +---@class neotree.Config.SourceSelector +---@field winbar boolean? +---@field statusline boolean? +---@field show_scrolled_off_parent_node boolean? +---@field sources neotree.Config.SourceSelector.Item[]? +---@field content_layout? "start"|"end"|"center" +---@field tabs_layout? "equal"|"start"|"end"|"center"|"focus" +---@field truncation_character string +---@field tabs_min_width integer? +---@field tabs_max_width integer? +---@field padding integer|{left: integer, right:integer}? +---@field separator neotree.Config.SourceSelector.Separator? +---@field separator_active neotree.Config.SourceSelector.Separator? +---@field show_separator_on_edge boolean? +---@field highlight_tab string? +---@field highlight_tab_active string? +---@field highlight_background string? +---@field highlight_separator string? +---@field highlight_separator_active string? + +---@class neotree.Config.GitStatusAsync +---@field batch_size integer? +---@field batch_delay integer? +---@field max_lines integer? + +---@class neotree.Config.Window.Size +---@field height string|number? +---@field width string|number? + +---@class neotree.Config.Window.Popup +---@field title (fun(state:table):string)? +---@field size neotree.Config.Window.Size? +---@field border neotree.Config.BorderStyle? + +---@alias neotree.Config.TreeCommand string|neotree.TreeCommand|neotree.Config.Window.Command.Configured + +---@class (exact) neotree.Config.Commands +---@field [string] function + +---@class (exact) neotree.Config.Window.Mappings +---@field [string] neotree.Config.TreeCommand? + +---@class neotree.Config.Window +---@field position string? +---@field width integer? +---@field height integer? +---@field auto_expand_width boolean? +---@field popup neotree.Config.Window.Popup? +---@field insert_as "child"|"sibling"|nil +---@field mapping_options neotree.Config.Mapping.Options? +---@field mappings neotree.Config.Window.Mappings? + +---@class neotree.Config.Renderers +---@field directory neotree.Component.Common[]? +---@field file neotree.Component.Common[]? +---@field message neotree.Component.Common[]? +---@field terminal neotree.Component.Common[]? + +---@class neotree.Config.ComponentDefaults +---@field container neotree.Component.Common.Container? +---@field indent neotree.Component.Common.Indent? +---@field icon neotree.Component.Common.Icon? +---@field modified neotree.Component.Common.Modified? +---@field name neotree.Component.Common.Name? +---@field git_status neotree.Component.Common.GitStatus? +---@field file_size neotree.Component.Common.FileSize? +---@field type neotree.Component.Common.Type? +---@field last_modified neotree.Component.Common.LastModified? +---@field created neotree.Component.Common.Created? +---@field symlink_target neotree.Component.Common.SymlinkTarget? + +---@alias neotree.Config.BorderStyle "NC"|"rounded"|"single"|"solid"|"double"|"" + +---@alias neotree.Config.SortFunction fun(a: NuiTree.Node, b: NuiTree.Node):boolean? + +---@class (exact) neotree.Config.Base +---@field sources string[] +---@field add_blank_line_at_top boolean +---@field auto_clean_after_session_restore boolean +---@field close_if_last_window boolean +---@field default_source string +---@field enable_diagnostics boolean +---@field enable_git_status boolean +---@field enable_modified_markers boolean +---@field enable_opened_markers boolean +---@field enable_refresh_on_write boolean +---@field enable_cursor_hijack boolean +---@field git_status_async boolean +---@field git_status_async_options neotree.Config.GitStatusAsync +---@field hide_root_node boolean +---@field retain_hidden_root_indent boolean +---@field log_level "trace"|"debug"|"info"|"warn"|"error"|"fatal"|nil +---@field log_to_file boolean|string +---@field open_files_in_last_window boolean +---@field open_files_do_not_replace_types string[] +---@field open_files_using_relative_paths boolean +---@field popup_border_style neotree.Config.BorderStyle +---@field resize_timer_interval integer|-1 +---@field sort_case_insensitive boolean +---@field sort_function? neotree.Config.SortFunction +---@field use_popups_for_input boolean +---@field use_default_mappings boolean +---@field source_selector neotree.Config.SourceSelector +---@field event_handlers? neotree.event.Handler[] +---@field default_component_configs neotree.Config.ComponentDefaults +---@field renderers neotree.Config.Renderers +---@field nesting_rules neotree.filenesting.Rule[] +---@field commands table +---@field window neotree.Config.Window +--- +---@field filesystem neotree.Config.Filesystem +---@field buffers neotree.Config.Buffers +---@field git_status neotree.Config.GitStatus +---@field document_symbols neotree.Config.DocumentSymbols +---@field bind_to_cwd boolean? + +---@class (partial) neotree.Config : neotree.Config.Base diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/events.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/events.lua new file mode 100644 index 0000000..716353f --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/events.lua @@ -0,0 +1,50 @@ +---@meta + +---@enum neotree.EventName +local _ = { + AFTER_RENDER = "after_render", + BEFORE_FILE_ADD = "before_file_add", + BEFORE_FILE_DELETE = "before_file_delete", + BEFORE_FILE_MOVE = "before_file_move", + BEFORE_FILE_RENAME = "before_file_rename", + BEFORE_RENDER = "before_render", + FILE_ADDED = "file_added", + FILE_DELETED = "file_deleted", + FILE_MOVED = "file_moved", + FILE_OPENED = "file_opened", + FILE_OPEN_REQUESTED = "file_open_requested", + FILE_RENAMED = "file_renamed", + FS_EVENT = "fs_event", + GIT_EVENT = "git_event", + GIT_STATUS_CHANGED = "git_status_changed", + STATE_CREATED = "state_created", + NEO_TREE_BUFFER_ENTER = "neo_tree_buffer_enter", + NEO_TREE_BUFFER_LEAVE = "neo_tree_buffer_leave", + NEO_TREE_LSP_UPDATE = "neo_tree_lsp_update", + NEO_TREE_POPUP_BUFFER_ENTER = "neo_tree_popup_buffer_enter", + NEO_TREE_POPUP_BUFFER_LEAVE = "neo_tree_popup_buffer_leave", + NEO_TREE_POPUP_INPUT_READY = "neo_tree_popup_input_ready", + NEO_TREE_WINDOW_AFTER_CLOSE = "neo_tree_window_after_close", + NEO_TREE_WINDOW_AFTER_OPEN = "neo_tree_window_after_open", + NEO_TREE_WINDOW_BEFORE_CLOSE = "neo_tree_window_before_close", + NEO_TREE_WINDOW_BEFORE_OPEN = "neo_tree_window_before_open", + VIM_AFTER_SESSION_LOAD = "vim_after_session_load", + VIM_BUFFER_ADDED = "vim_buffer_added", + VIM_BUFFER_CHANGED = "vim_buffer_changed", + VIM_BUFFER_DELETED = "vim_buffer_deleted", + VIM_BUFFER_ENTER = "vim_buffer_enter", + VIM_BUFFER_MODIFIED_SET = "vim_buffer_modified_set", + VIM_COLORSCHEME = "vim_colorscheme", + VIM_CURSOR_MOVED = "vim_cursor_moved", + VIM_DIAGNOSTIC_CHANGED = "vim_diagnostic_changed", + VIM_DIR_CHANGED = "vim_dir_changed", + VIM_INSERT_LEAVE = "vim_insert_leave", + VIM_LEAVE = "vim_leave", + VIM_LSP_REQUEST = "vim_lsp_request", + VIM_RESIZED = "vim_resized", + VIM_TAB_CLOSED = "vim_tab_closed", + VIM_TERMINAL_ENTER = "vim_terminal_enter", + VIM_TEXT_CHANGED_NORMAL = "vim_text_changed_normal", + VIM_WIN_CLOSED = "vim_win_closed", + VIM_WIN_ENTER = "vim_win_enter", +} diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/fixes/compat-0.10.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/fixes/compat-0.10.lua new file mode 100644 index 0000000..719ea95 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/fixes/compat-0.10.lua @@ -0,0 +1,11 @@ +---@meta +--- A backport from nightly for v0.10 type checking + +--- @class neotree._vim.api.keyset.create_autocmd.callback_args +--- @field id integer autocommand id +--- @field event string name of the triggered event |autocmd-events| +--- @field group? integer autocommand group id, if any +--- @field match string expanded value of +--- @field buf integer expanded value of +--- @field file string expanded value of +--- @field data? any arbitrary data passed from |nvim_exec_autocmds()| *event-data* diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/fixes/uv.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/fixes/uv.lua new file mode 100644 index 0000000..5daf796 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/types/fixes/uv.lua @@ -0,0 +1,23 @@ +---@meta + +---@class uv +---@field constants {O_RDONLY: integer, O_WRONLY: integer, O_RDWR: integer, O_APPEND: integer, O_CREAT: integer, O_DSYNC: integer, O_EXCL: integer, O_NOCTTY: integer, O_NONBLOCK: integer, O_RSYNC: integer, O_SYNC: integer, O_TRUNC: integer, SOCK_STREAM: integer, SOCK_DGRAM: integer, SOCK_SEQPACKET: integer, SOCK_RAW: integer, SOCK_RDM: integer, AF_UNIX: integer, AF_INET: integer, AF_INET6: integer, AF_IPX: integer, AF_NETLINK: integer, AF_X25: integer, AF_AX25: integer, AF_ATMPVC: integer, AF_APPLETALK: integer, AF_PACKET: integer, AI_ADDRCONFIG: integer, AI_V4MAPPED: integer, AI_ALL: integer, AI_NUMERICHOST: integer, AI_PASSIVE: integer, AI_NUMERICSERV: integer, SIGHUP: integer, SIGINT: integer, SIGQUIT: integer, SIGILL: integer, SIGTRAP: integer, SIGABRT: integer, SIGIOT: integer, SIGBUS: integer, SIGFPE: integer, SIGKILL: integer, SIGUSR1: integer, SIGSEGV: integer, SIGUSR2: integer, SIGPIPE: integer, SIGALRM: integer, SIGTERM: integer, SIGCHLD: integer, SIGSTKFLT: integer, SIGCONT: integer, SIGSTOP: integer, SIGTSTP: integer, SIGTTIN: integer, SIGWINCH: integer, SIGIO: integer, SIGPOLL: integer, SIGXFSZ: integer, SIGVTALRM: integer, SIGPROF: integer, UDP_RECVMMSG: integer, UDP_MMSG_CHUNK: integer, UDP_REUSEADDR: integer, UDP_PARTIAL: integer, UDP_IPV6ONLY: integer, TCP_IPV6ONLY: integer, UDP_MMSG_FREE: integer, SIGSYS: integer, SIGPWR: integer, SIGTTOU: integer, SIGURG: integer, SIGXCPU: integer} +local uv = {} + +--- Opens path as a directory stream. Returns a handle that the user can pass to +--- `uv.fs_readdir()`. The `entries` parameter defines the maximum number of entries +--- that should be returned by each call to `uv.fs_readdir()`. +--- +--- **Returns (sync version):** `luv_dir_t userdata` or `fail` +--- +--- **Returns (async version):** `uv_fs_t userdata` +--- +---@param path string +---@param callback nil +---@param entries integer? +---@return uv.luv_dir_t|nil dir +---@return uv.error.message|nil err +---@return uv.error.name|nil err_name +--- +---@overload fun(path: string, callback: uv.fs_opendir.callback, entries?: integer):uv.uv_fs_t +function uv.fs_opendir(path, callback, entries) end diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/highlights.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/highlights.lua new file mode 100644 index 0000000..5cee7c4 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/highlights.lua @@ -0,0 +1,324 @@ +local log = require("neo-tree.log") +local utils = require("neo-tree.utils") +local M = {} + +---@type integer +M.ns_id = vim.api.nvim_create_namespace("neo-tree.nvim") + +M.BUFFER_NUMBER = "NeoTreeBufferNumber" +M.CURSOR_LINE = "NeoTreeCursorLine" +M.DIM_TEXT = "NeoTreeDimText" +M.DIRECTORY_ICON = "NeoTreeDirectoryIcon" +M.DIRECTORY_NAME = "NeoTreeDirectoryName" +M.DOTFILE = "NeoTreeDotfile" +M.FADE_TEXT_1 = "NeoTreeFadeText1" +M.FADE_TEXT_2 = "NeoTreeFadeText2" +M.FILE_ICON = "NeoTreeFileIcon" +M.FILE_NAME = "NeoTreeFileName" +M.FILE_NAME_OPENED = "NeoTreeFileNameOpened" +M.FILE_STATS = "NeoTreeFileStats" +M.FILE_STATS_HEADER = "NeoTreeFileStatsHeader" +M.FILTER_TERM = "NeoTreeFilterTerm" +M.FLOAT_BORDER = "NeoTreeFloatBorder" +M.FLOAT_NORMAL = "NeoTreeFloatNormal" +M.FLOAT_TITLE = "NeoTreeFloatTitle" +M.GIT_ADDED = "NeoTreeGitAdded" +M.GIT_CONFLICT = "NeoTreeGitConflict" +M.GIT_DELETED = "NeoTreeGitDeleted" +M.GIT_IGNORED = "NeoTreeGitIgnored" +M.GIT_MODIFIED = "NeoTreeGitModified" +M.GIT_RENAMED = "NeoTreeGitRenamed" +M.GIT_STAGED = "NeoTreeGitStaged" +M.GIT_UNTRACKED = "NeoTreeGitUntracked" +M.GIT_UNSTAGED = "NeoTreeGitUnstaged" +M.HIDDEN_BY_NAME = "NeoTreeHiddenByName" +M.INDENT_MARKER = "NeoTreeIndentMarker" +M.MESSAGE = "NeoTreeMessage" +M.MODIFIED = "NeoTreeModified" +M.NORMAL = "NeoTreeNormal" +M.NORMALNC = "NeoTreeNormalNC" +M.SIGNCOLUMN = "NeoTreeSignColumn" +M.STATUS_LINE = "NeoTreeStatusLine" +M.STATUS_LINE_NC = "NeoTreeStatusLineNC" +M.TAB_ACTIVE = "NeoTreeTabActive" +M.TAB_INACTIVE = "NeoTreeTabInactive" +M.TAB_SEPARATOR_ACTIVE = "NeoTreeTabSeparatorActive" +M.TAB_SEPARATOR_INACTIVE = "NeoTreeTabSeparatorInactive" +M.VERTSPLIT = "NeoTreeVertSplit" +M.WINSEPARATOR = "NeoTreeWinSeparator" +M.END_OF_BUFFER = "NeoTreeEndOfBuffer" +M.ROOT_NAME = "NeoTreeRootName" +M.SYMBOLIC_LINK_TARGET = "NeoTreeSymbolicLinkTarget" +M.TITLE_BAR = "NeoTreeTitleBar" +M.EXPANDER = "NeoTreeExpander" +M.WINDOWS_HIDDEN = "NeoTreeWindowsHidden" +M.PREVIEW = "NeoTreePreview" + +---@param n integer +---@param chars integer? +local function dec_to_hex(n, chars) + chars = chars or 6 + local hex = string.format("%0" .. chars .. "x", n) + while #hex < chars do + hex = "0" .. hex + end + return hex +end + +---@param name string +local get_hl_by_name = function(name) + if vim.api.nvim_get_hl then + local hl = vim.api.nvim_get_hl(0, { name = name }) + ---@diagnostic disable-next-line: inject-field + hl.foreground = hl.fg + ---@diagnostic disable-next-line: inject-field + hl.background = hl.bg + return hl + end + ---TODO: remove in 4.0 + ---@diagnostic disable-next-line: deprecated + return vim.api.nvim_get_hl_by_name(name, true) +end +---If the given highlight group is not defined, define it. +---@param hl_group_name string The name of the highlight group. +---@param link_to_if_exists string[] A list of highlight groups to link to, in order of priority. The first one that exists will be used. +---@param background string? The background color to use, in hex, if the highlight group is not defined and it is not linked to another group. +---@param foreground string? The foreground color to use, in hex, if the highlight group is not defined and it is not linked to another group. +---@param gui string? The gui to use, if the highlight group is not defined and it is not linked to another group. +---@return table hlgroups The highlight group values. +M.create_highlight_group = function(hl_group_name, link_to_if_exists, background, foreground, gui) + local success, hl_group = pcall(get_hl_by_name, hl_group_name, true) + if not success or not hl_group.foreground or not hl_group.background then + for _, link_to in ipairs(link_to_if_exists) do + success, hl_group = pcall(get_hl_by_name, link_to, true) + if success then + local new_group_has_settings = background or foreground or gui + local link_to_has_settings = hl_group.foreground or hl_group.background + if link_to_has_settings or not new_group_has_settings then + vim.cmd("highlight default link " .. hl_group_name .. " " .. link_to) + return hl_group + end + end + end + + if type(background) == "number" then + background = dec_to_hex(background) + end + if type(foreground) == "number" then + foreground = dec_to_hex(foreground) + end + + local cmd = "highlight default " .. hl_group_name + if background then + cmd = cmd .. " guibg=#" .. background + end + if foreground then + cmd = cmd .. " guifg=#" .. foreground + else + cmd = cmd .. " guifg=NONE" + end + if gui then + cmd = cmd .. " gui=" .. gui + end + vim.cmd(cmd) + + return { + background = background and tonumber(background, 16) or nil, + foreground = foreground and tonumber(foreground, 16) or nil, + } + end + return hl_group +end + +---@param hl_group_name string +---@param fade_percentage number +local calculate_faded_highlight_group = function(hl_group_name, fade_percentage) + local normal = get_hl_by_name("Normal") + if type(normal.foreground) ~= "number" then + if vim.go.background == "dark" then + normal.foreground = 0xffffff + else + normal.foreground = 0x000000 + end + end + if type(normal.background) ~= "number" then + if vim.go.background == "dark" then + normal.background = 0x000000 + else + normal.background = 0xffffff + end + end + local foreground = dec_to_hex(normal.foreground) + local background = dec_to_hex(normal.background) + + local hl_group = get_hl_by_name(hl_group_name) + if type(hl_group.foreground) == "number" then + foreground = dec_to_hex(hl_group.foreground) + end + if type(hl_group.background) == "number" then + background = dec_to_hex(hl_group.background) + end + + local gui = {} + if hl_group.bold then + table.insert(gui, "bold") + end + if hl_group.italic then + table.insert(gui, "italic") + end + if hl_group.underline then + table.insert(gui, "underline") + end + if hl_group.undercurl then + table.insert(gui, "undercurl") + end + + local hl + if #gui > 0 then + hl = table.concat(gui, ",") + end + + local f_red = tonumber(foreground:sub(1, 2), 16) + local f_green = tonumber(foreground:sub(3, 4), 16) + local f_blue = tonumber(foreground:sub(5, 6), 16) + + local b_red = tonumber(background:sub(1, 2), 16) + local b_green = tonumber(background:sub(3, 4), 16) + local b_blue = tonumber(background:sub(5, 6), 16) + + local red = (f_red * fade_percentage) + (b_red * (1 - fade_percentage)) + local green = (f_green * fade_percentage) + (b_green * (1 - fade_percentage)) + local blue = (f_blue * fade_percentage) + (b_blue * (1 - fade_percentage)) + + local new_foreground = + string.format("%s%s%s", dec_to_hex(red, 2), dec_to_hex(green, 2), dec_to_hex(blue, 2)) + + return { + background = hl_group.background, + foreground = new_foreground, + gui = hl, + } +end + +local faded_highlight_group_cache = {} +---@param hl_group_name string +---@param fade_percentage number +M.get_faded_highlight_group = function(hl_group_name, fade_percentage) + if type(hl_group_name) ~= "string" then + error("hl_group_name must be a string") + end + if type(fade_percentage) ~= "number" then + error("hl_group_name must be a number") + end + if fade_percentage < 0 or fade_percentage > 1 then + error("fade_percentage must be between 0 and 1") + end + + local key = hl_group_name .. "_" .. tostring(math.floor(fade_percentage * 100)) + if faded_highlight_group_cache[key] then + return faded_highlight_group_cache[key] + end + + local faded = calculate_faded_highlight_group(hl_group_name, fade_percentage) + + M.create_highlight_group(key, {}, faded.background, faded.foreground, faded.gui) + faded_highlight_group_cache[key] = key + return key +end +local nvim_0_10 = vim.fn.has("nvim-0.10") +M.setup = function() + local added_hl_name = nvim_0_10 and "Added" or "diffAdded" + local changed_hl_name = nvim_0_10 and "Changed" or "diffChanged" + local removed_hl_name = nvim_0_10 and "Removed" or "diffRemoved" + -- Reset this here in case of color scheme change + faded_highlight_group_cache = {} + + local normal_hl = M.create_highlight_group(M.NORMAL, { "Normal" }) + local normalnc_hl = M.create_highlight_group(M.NORMALNC, { "NormalNC", M.NORMAL }) + + M.create_highlight_group(M.SIGNCOLUMN, { "SignColumn", M.NORMAL }) + + M.create_highlight_group(M.STATUS_LINE, { "StatusLine" }) + M.create_highlight_group(M.STATUS_LINE_NC, { "StatusLineNC" }) + + M.create_highlight_group(M.VERTSPLIT, { "VertSplit" }) + M.create_highlight_group(M.WINSEPARATOR, { "WinSeparator" }) + + M.create_highlight_group(M.END_OF_BUFFER, { "EndOfBuffer" }) + + local float_border_hl = + M.create_highlight_group(M.FLOAT_BORDER, { "FloatBorder" }, normalnc_hl.background, "444444") + + M.create_highlight_group(M.FLOAT_NORMAL, { "NormalFloat", M.NORMAL }) + + M.create_highlight_group(M.FLOAT_TITLE, {}, float_border_hl.background, normal_hl.foreground) + + local title_fg = normal_hl.background + if title_fg == float_border_hl.foreground then + title_fg = normal_hl.foreground + end + M.create_highlight_group(M.TITLE_BAR, {}, float_border_hl.foreground, title_fg) + + local dim_text = calculate_faded_highlight_group("NeoTreeNormal", 0.3) + + M.create_highlight_group(M.BUFFER_NUMBER, { "SpecialChar" }) + --M.create_highlight_group(M.DIM_TEXT, {}, nil, "505050") + M.create_highlight_group(M.MESSAGE, {}, nil, dim_text.foreground, "italic") + M.create_highlight_group(M.FADE_TEXT_1, {}, nil, "626262") + M.create_highlight_group(M.FADE_TEXT_2, {}, nil, "444444") + M.create_highlight_group(M.DOTFILE, {}, nil, "626262") + M.create_highlight_group(M.HIDDEN_BY_NAME, { M.DOTFILE }, nil, nil) + M.create_highlight_group(M.CURSOR_LINE, { "CursorLine" }, nil, nil, "bold") + M.create_highlight_group(M.DIM_TEXT, {}, nil, dim_text.foreground) + M.create_highlight_group(M.DIRECTORY_NAME, { "Directory" }, "NONE", "NONE") + M.create_highlight_group(M.DIRECTORY_ICON, { "Directory" }, nil, "73cef4") + M.create_highlight_group(M.FILE_ICON, { M.DIRECTORY_ICON }) + M.create_highlight_group(M.FILE_NAME, {}, "NONE", "NONE") + M.create_highlight_group(M.FILE_NAME_OPENED, {}, nil, nil, "bold") + M.create_highlight_group(M.SYMBOLIC_LINK_TARGET, { M.FILE_NAME }) + M.create_highlight_group(M.FILTER_TERM, { "SpecialChar", "Normal" }) + M.create_highlight_group(M.ROOT_NAME, {}, nil, nil, "bold,italic") + M.create_highlight_group(M.INDENT_MARKER, { M.DIM_TEXT }) + M.create_highlight_group(M.EXPANDER, { M.DIM_TEXT }) + M.create_highlight_group(M.MODIFIED, {}, nil, "d7d787") + M.create_highlight_group(M.WINDOWS_HIDDEN, { M.DOTFILE }, nil, nil) + M.create_highlight_group(M.PREVIEW, { "Search" }, nil, nil) + + M.create_highlight_group( + M.GIT_ADDED, + { "GitGutterAdd", "GitSignsAdd", added_hl_name }, + nil, + "5faf5f" + ) + M.create_highlight_group( + M.GIT_DELETED, + { "GitGutterDelete", "GitSignsDelete", removed_hl_name }, + nil, + "ff5900" + ) + M.create_highlight_group( + M.GIT_MODIFIED, + { "GitGutterChange", "GitSignsChange", changed_hl_name }, + nil, + "d7af5f" + ) + local conflict = M.create_highlight_group(M.GIT_CONFLICT, {}, nil, "ff8700", "italic,bold") + M.create_highlight_group(M.GIT_IGNORED, { M.DOTFILE }, nil, nil) + M.create_highlight_group(M.GIT_RENAMED, { M.GIT_MODIFIED }, nil, nil) + M.create_highlight_group(M.GIT_STAGED, { M.GIT_ADDED }, nil, nil) + M.create_highlight_group(M.GIT_UNSTAGED, { M.GIT_CONFLICT }, nil, nil) + M.create_highlight_group(M.GIT_UNTRACKED, {}, nil, conflict.foreground, "italic") + + M.create_highlight_group(M.TAB_ACTIVE, {}, nil, nil, "bold") + M.create_highlight_group(M.TAB_INACTIVE, {}, "141414", "777777") + M.create_highlight_group(M.TAB_SEPARATOR_ACTIVE, {}, nil, "0a0a0a") + M.create_highlight_group(M.TAB_SEPARATOR_INACTIVE, {}, "141414", "101010") + + local faded_normal = calculate_faded_highlight_group("NeoTreeNormal", 0.4) + M.create_highlight_group(M.FILE_STATS, {}, nil, faded_normal.foreground) + + local faded_root = calculate_faded_highlight_group("NeoTreeRootName", 0.5) + M.create_highlight_group(M.FILE_STATS_HEADER, {}, nil, faded_root.foreground, faded_root.gui) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/inputs.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/inputs.lua new file mode 100644 index 0000000..e62b581 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/inputs.lua @@ -0,0 +1,109 @@ +local NuiInput = require("nui.input") +local nt = require("neo-tree") +local popups = require("neo-tree.ui.popups") +local events = require("neo-tree.events") + +local M = {} + +---@param input NuiInput +---@param callback function? +M.show_input = function(input, callback) + input:mount() + + input:map("i", "", function() + vim.cmd("stopinsert") + input:unmount() + end, { noremap = true }) + + input:map("n", "", function() + input:unmount() + end, { noremap = true }) + + input:map("n", "q", function() + input:unmount() + end, { noremap = true }) + + input:map("i", "", "", { noremap = true }) + + local event = require("nui.utils.autocmd").event + input:on({ event.BufLeave, event.BufDelete }, function() + input:unmount() + if callback then + callback() + end + end, { once = true }) + + if input.prompt_type ~= "confirm" then + vim.schedule(function() + events.fire_event(events.NEO_TREE_POPUP_INPUT_READY, { + bufnr = input.bufnr, + winid = input.winid, + }) + end) + end +end + +---@param message string +---@param default_value string? +---@param callback function +---@param options nui_popup_options? +---@param completion string? +M.input = function(message, default_value, callback, options, completion) + if nt.config.use_popups_for_input then + local popup_options = popups.popup_options(message, 10, options) + + local input = NuiInput(popup_options, { + prompt = " ", + default_value = default_value, + on_submit = callback, + }) + + M.show_input(input) + else + local opts = { + prompt = message .. "\n", + default = default_value, + } + if vim.opt.cmdheight:get() == 0 then + -- NOTE: I really don't know why but letters before the first '\n' is not rendered except in noice.nvim + -- when vim.opt.cmdheight = 0 <2023-10-24, pysan3> + opts.prompt = "Neo-tree Popup\n" .. opts.prompt + end + if completion then + opts.completion = completion + end + vim.ui.input(opts, callback) + end +end + +---Blocks if callback is omitted +---@param message string +---@param callback? fun(confirmed: boolean) +---@return boolean? confirmed_if_no_callback +M.confirm = function(message, callback) + if callback then + if nt.config.use_popups_for_input then + local popup_options = popups.popup_options(message, 10) + + ---@class NuiInput + local input = NuiInput(popup_options, { + prompt = " y/n: ", + on_close = function() + callback(false) + end, + on_submit = function(value) + callback(value == "y" or value == "Y") + end, + }) + + input.prompt_type = "confirm" + M.show_input(input) + else + callback(vim.fn.confirm(message, "&Yes\n&No") == 1) + end + else + return vim.fn.confirm(message, "&Yes\n&No") == 1 + end +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/popups.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/popups.lua new file mode 100644 index 0000000..79cf616 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/popups.lua @@ -0,0 +1,147 @@ +local NuiText = require("nui.text") +local NuiPopup = require("nui.popup") +local nt = require("neo-tree") +local highlights = require("neo-tree.ui.highlights") +local log = require("neo-tree.log") + +local M = {} + +local winborder_option_exists = vim.fn.exists("&winborder") > 0 +-- These borders will cause errors when trying to display border text with them +local invalid_borders = { "", "none", "shadow" } +---@param title string +---@param min_width integer? +---@param override_options table? +M.popup_options = function(title, min_width, override_options) + if string.len(title) ~= 0 then + title = " " .. title .. " " + end + min_width = min_width or 30 + local width = string.len(title) + 2 + + local popup_border_style = nt.config.popup_border_style + if popup_border_style == "" then + -- Try to use winborder + if not winborder_option_exists or vim.tbl_contains(invalid_borders, vim.o.winborder) then + popup_border_style = "single" + else + ---@diagnostic disable-next-line: cast-local-type + popup_border_style = vim.o.winborder + end + end + local popup_border_text = NuiText(title, highlights.FLOAT_TITLE) + local col = 0 + -- fix popup position when using multigrid + local popup_last_col = vim.api.nvim_win_get_position(0)[2] + width + 2 + if popup_last_col >= vim.o.columns then + col = vim.o.columns - popup_last_col + end + ---@type nui_popup_options + local popup_options = { + ns_id = highlights.ns_id, + relative = "cursor", + position = { + row = 1, + col = col, + }, + size = width, + border = { + text = { + top = popup_border_text, + }, + ---@diagnostic disable-next-line: assign-type-mismatch + style = popup_border_style, + highlight = highlights.FLOAT_BORDER, + }, + win_options = { + winhighlight = "Normal:" + .. highlights.FLOAT_NORMAL + .. ",FloatBorder:" + .. highlights.FLOAT_BORDER, + }, + buf_options = { + bufhidden = "delete", + buflisted = false, + filetype = "neo-tree-popup", + }, + } + + if popup_border_style == "NC" then + local blank = NuiText(" ", highlights.TITLE_BAR) + popup_border_text = NuiText(title, highlights.TITLE_BAR) + popup_options.border = { + style = { "▕", blank, "▏", "▏", " ", "▔", " ", "▕" }, + highlight = highlights.FLOAT_BORDER, + text = { + top = popup_border_text, + top_align = "left", + }, + } + end + + if override_options then + return vim.tbl_extend("force", popup_options, override_options) + else + return popup_options + end +end + +---@param title string +---@param message elem_or_list +---@param size integer? +M.alert = function(title, message, size) + local lines = {} + local max_line_width = title:len() + ---@param line any + local add_line = function(line) + line = tostring(line) + if line:len() > max_line_width then + max_line_width = line:len() + end + table.insert(lines, line) + end + + if type(message) == "table" then + for _, v in ipairs(message) do + add_line(v) + end + else + add_line(message) + end + + add_line("") + add_line(" Press or to close") + + local win_options = M.popup_options(title, 80) + win_options.zindex = 60 + win_options.size = { + width = max_line_width + 4, + height = #lines + 1, + } + local win = NuiPopup(win_options) + win:mount() + + local success, msg = pcall(vim.api.nvim_buf_set_lines, win.bufnr, 0, 0, false, lines) + if success then + win:map("n", "", function() + win:unmount() + end, { noremap = true }) + + win:map("n", "", function() + win:unmount() + end, { noremap = true }) + + local event = require("nui.utils.autocmd").event + win:on({ event.BufLeave, event.BufDelete }, function() + win:unmount() + end, { once = true }) + + -- why is this necessary? + vim.api.nvim_set_current_win(win.winid) + else + log.error(msg) + win:unmount() + end +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/renderer.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/renderer.lua new file mode 100644 index 0000000..04070a5 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/renderer.lua @@ -0,0 +1,1411 @@ +local NuiLine = require("nui.line") +local NuiTree = require("nui.tree") +local NuiSplit = require("nui.split") +local NuiPopup = require("nui.popup") +local utils = require("neo-tree.utils") +local highlights = require("neo-tree.ui.highlights") +local popups = require("neo-tree.ui.popups") +local events = require("neo-tree.events") +local keymap = require("nui.utils.keymap") +local autocmd = require("nui.utils.autocmd") +local log = require("neo-tree.log") +local windows = require("neo-tree.ui.windows") +local nt = require("neo-tree") + +local M = { resize_timer_interval = 50 } +local ESC_KEY = utils.keycode("") +local default_popup_size = { width = 60, height = "80%" } +local draw, create_tree, render_tree + +local floating_windows = {} +local update_floating_windows = function() + local valid_windows = {} + for _, win in ipairs(floating_windows) do + if M.is_window_valid(win.winid) then + table.insert(valid_windows, win) + end + end + floating_windows = valid_windows +end + +local tabid_to_tabnr = function(tabid) + return vim.api.nvim_tabpage_is_valid(tabid) and vim.api.nvim_tabpage_get_number(tabid) +end + +local buffer_is_usable = function(bufnr) + return vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr) +end + +local cleaned_up = false +---Clean up invalid neotree buffers (e.g after a session restore) +---@param force boolean if true, force cleanup. Otherwise only cleanup once +M.clean_invalid_neotree_buffers = function(force) + if cleaned_up and not force then + return + end + + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + local bufname = vim.fn.bufname(buf) + local is_neotree_buffer = string.match(bufname, "neo%-tree [^ ]+ %[%d+]") + local is_valid_neotree, _ = pcall(vim.api.nvim_buf_get_var, buf, "neo_tree_source") + if is_neotree_buffer and not is_valid_neotree then + vim.api.nvim_buf_delete(buf, { force = true }) + end + end + cleaned_up = true +end + +local resize_monitor_timer = nil +local start_resize_monitor = function() + local interval = M.resize_timer_interval or -1 + if interval < 0 then + return + end + if type(interval) ~= "number" then + log.warn("Invalid resize_timer_interval:", interval) + return + end + if resize_monitor_timer then + return + end + local manager = require("neo-tree.sources.manager") + local check_window_size + local speed_up_loops = 0 + check_window_size = function() + local windows_exist = false + local success, err = pcall(manager._for_each_state, nil, function(state) + if state.win_width and M.tree_is_visible(state) then + windows_exist = true + local current_size = utils.get_inner_win_width(state.winid) + if current_size ~= state.win_width then + log.trace("Window size changed, redrawing tree") + state.win_width = current_size + render_tree(state) + speed_up_loops = 21 -- move to fast timer for the next 1000 ms + end + end + end) + + speed_up_loops = speed_up_loops - 1 + if success then + if windows_exist then + local this_interval = interval + if speed_up_loops > 0 then + this_interval = 50 + else + speed_up_loops = 0 + end + vim.defer_fn(check_window_size, this_interval) + else + log.trace("No windows exist, stopping resize monitor") + end + else + log.debug("Error checking window size: ", err) + vim.defer_fn(check_window_size, math.max(interval * 5, 1000)) + end + end + + vim.defer_fn(check_window_size, interval) +end + +---Safely closes the window and deletes the buffer associated with the state +---@param state neotree.State State of the source to close +---@param focus_prior_window boolean | nil if true or nil, focus the window that was previously focused +M.close = function(state, focus_prior_window) + log.debug("Closing window, but saving position first.") + M.position.save(state) + + if focus_prior_window == nil then + focus_prior_window = true + end + local window_existed = false + if state and state.winid then + if M.window_exists(state) then + local bufnr = vim.api.nvim_win_get_buf(state.winid) + -- if bufnr is different then we expect, then it was taken over by + -- another buffer, so we can't delete it now + if bufnr == state.bufnr then + window_existed = true + if state.current_position == "current" then + -- we are going to hide the buffer instead of closing the window + M.position.save(state) + local new_buf = vim.fn.bufnr("#") + if new_buf < 1 then + new_buf = vim.api.nvim_create_buf(true, false) + end + vim.api.nvim_win_set_buf(state.winid, new_buf) + else + local args = { + position = state.current_position, + source = state.name, + winid = state.winid, + tabnr = tabid_to_tabnr(state.tabid), -- for compatibility + tabid = state.tabid, + } + events.fire_event(events.NEO_TREE_WINDOW_BEFORE_CLOSE, args) + local win_list = vim.api.nvim_tabpage_list_wins(0) + if focus_prior_window and #win_list > 1 then + -- focus the prior used window if we are closing the currently focused window + local current_winid = vim.api.nvim_get_current_win() + if current_winid == state.winid then + local pwin = require("neo-tree").get_prior_window() + if type(pwin) == "number" and pwin > 0 then + pcall(vim.api.nvim_set_current_win, pwin) + end + end + end + -- if the window was a float, changing the current win would have closed it already + pcall(vim.api.nvim_win_close, state.winid, true) + events.fire_event(events.NEO_TREE_WINDOW_AFTER_CLOSE, args) + end + end + end + state.winid = nil + end + local bufnr = utils.get_value(state, "bufnr", 0, true) + if bufnr > 0 and vim.api.nvim_buf_is_valid(bufnr) then + state.bufnr = nil + local success, err = pcall(vim.api.nvim_buf_delete, bufnr, { force = true }) + if not success and err and err:match("E523") then + vim.schedule_wrap(function() + vim.api.nvim_buf_delete(bufnr, { force = true }) + end)() + end + end + return window_existed +end + +M.close_floating_window = function(source_name) + local found_windows = {} + for _, win in ipairs(floating_windows) do + if win.source_name == source_name then + table.insert(found_windows, win) + end + end + + local valid_window_was_closed = false + for _, win in ipairs(found_windows) do + if not valid_window_was_closed then + valid_window_was_closed = M.is_window_valid(win.winid) + end + -- regardless of whether the window is valid or not, nui will cleanup + win:unmount() + end + return valid_window_was_closed +end + +M.close_all_floating_windows = function() + while #floating_windows > 0 do + local win = table.remove(floating_windows) + win:unmount() + end +end + +M.get_nui_popup = function(winid) + for _, win in ipairs(floating_windows) do + if win.winid == winid then + return win + end + end +end + +---@param source_items neotree.FileItem[] +---@param filtered_items neotree.Config.Filesystem.FilteredItems +local remove_filtered = function(source_items, filtered_items) + local visible = {} + local hidden = {} + for _, item in ipairs(source_items) do + local fby = item.filtered_by + if not fby or item.is_reveal_target or item.contains_reveal_target then + visible[#visible + 1] = item + else + while fby do + if fby.never_show then + -- pretend it doesn't exist + break + elseif filtered_items.visible or item.is_nested or fby.always_show then + visible[#visible + 1] = item + break + elseif fby.name or fby.pattern or fby.dotfiles or fby.hidden then + hidden[#hidden + 1] = item + break + elseif fby.show_gitignored and fby.gitignored then + visible[#visible + 1] = item + break + elseif fby.parent then + fby = fby.parent + else + -- filtered by some other reason + hidden[#hidden + 1] = item + break + end + end + end + end + return visible, hidden +end + +---@class neotree.FileNodeData : neotree.FileItem + +---@class neotree.FileNode : neotree.FileNodeData, NuiTree.Node + +local create_nodes +---Transforms a list of items into a collection of TreeNodes. +---@param source_items neotree.FileItem[] The list of items to transform. The expected +--interface for these items depends on the component renderers configured for +--the given source, but they must contain at least an id field. +---@param state neotree.State The current state of the plugin. +---@param level integer Optional. The current level of the tree, defaults to 0. +---@return table nodes A collection of TreeNodes. +create_nodes = function(source_items, state, level) + level = level or 0 + local nodes = {} + local filtered_items = state.filtered_items or {} + local visible, hidden = remove_filtered(source_items, filtered_items) + + if #visible == 0 and level <= 1 and filtered_items.force_visible_in_empty_folder then + source_items = hidden + else + source_items = visible + end + + local show_indent_marker_for_message + local msg = state.renderers.message or {} + if msg[1] and msg[1][1] == "indent" then + local indent = msg[1] + ---these types might need a slight rework but this cast is correct. + ---@diagnostic disable-next-line: cast-type-mismatch + ---@cast indent neotree.Component.Common.Indent + show_indent_marker_for_message = indent.with_markers + end + + for i, item in ipairs(source_items) do + local is_last_child = i == #source_items + + local nodeData = { + id = item.id, + name = item.name, + type = item.type, + loaded = item.loaded, + filtered_by = item.filtered_by, + extra = item.extra, + is_nested = item.is_nested, + skip_node = item.skip_node, + is_empty_with_hidden_root = item.is_empty_with_hidden_root, + stat = item.stat, + stat_provider = item.stat_provider, + -- TODO: The below properties are not universal and should not be here. + -- Maybe they should be moved to the "extra" field? + is_link = item.is_link, + link_to = item.link_to, + path = item.path, + ext = item.ext, + search_pattern = item.search_pattern, + level = level, + is_last_child = is_last_child, + } + + local node_children = nil + if item.children ~= nil then + node_children = create_nodes(item.children, state, level + 1) + end + + local node = NuiTree.Node(nodeData, node_children) + if item._is_expanded then + node:expand() + end + table.insert(nodes, node) + end + + if #hidden > 0 then + if source_items == hidden then + local nodeData = { + id = hidden[#hidden].id .. "_hidden_message", + name = "(forced to show " + .. #hidden + .. " hidden " + .. (#hidden > 1 and "items" or "item") + .. ")", + type = "message", + level = level, + is_last_child = show_indent_marker_for_message, + } + local node = NuiTree.Node(nodeData) + table.insert(nodes, node) + elseif filtered_items.show_hidden_count or (#visible == 0 and level <= 1) then + local nodeData = { + id = hidden[#hidden].id .. "_hidden_message", + name = "(" .. #hidden .. " hidden " .. (#hidden > 1 and "items" or "item") .. ")", + type = "message", + level = level, + is_last_child = show_indent_marker_for_message, + } + if #nodes > 0 then + nodes[#nodes].is_last_child = not show_indent_marker_for_message + end + local node = NuiTree.Node(nodeData) + table.insert(nodes, node) + end + end + return nodes +end + +local one_line = function(text) + if type(text) == "string" then + return text:gsub("\n", " ") + else + return text + end +end + +M.render_component = function(component, item, state, remaining_width) + local component_func = state.components[component[1]] + if component_func then + local success, component_data, wanted_width = + pcall(component_func, component, item, state, remaining_width) + if success then + if component_data == nil then + return { {} } + end + if component_data.text then + -- everything else is easier if we make sure this is always the same shape + -- which is an array of { text, highlight } tables + component_data = { component_data } + end + for _, data in ipairs(component_data) do + data.text = one_line(data.text) + end + return component_data, wanted_width + else + local name = component[1] or "[missing_name]" + local msg = string.format("Error rendering component %s: %s", name, component_data) + log.warn(msg) + return { { text = msg, highlight = highlights.NORMAL } } + end + else + local name = component[1] or "[missing_name]" + local msg = "Neo-tree: Component " .. name .. " not found." + log.warn(msg) + return { { text = msg, highlight = highlights.NORMAL } } + end +end + +local prepare_node = function(item, state) + if item.skip_node then + if item.is_empty_with_hidden_root then + local line = NuiLine() + line:append("(empty folder)", highlights.MESSAGE) + return line + else + return nil + end + end + -- pre_render is used to calculate the longest node width + -- without actually rendering the node. + -- We'll try to reuse that work if possible. + local pre_render = state._in_pre_render + if item.line and not pre_render then + local line = item.line + -- Only use it once, we don't want to accidentally use stale data + item.line = nil + if + line + and item.wanted_width + and state.longest_node + and item.wanted_width <= state.longest_node + then + return line + end + end + ---@class NuiLine + local line = NuiLine() + + local renderer = state.renderers[item.type] + if not renderer then + line:append(item.type .. ": ", "Comment") + line:append(item.name) + return line + end + + local remaining_cols = state.win_width + if remaining_cols == nil then + if state.winid then + remaining_cols = vim.api.nvim_win_get_width(state.winid) + else + local default_width = utils.resolve_config_option(state, "window.width", 40) + remaining_cols = default_width + end + end + + local wanted_width = 0 + if state.current_position == "current" then + local longest = state.longest_node or 0 + remaining_cols = math.min(remaining_cols, longest + 4) + end + + local should_pad = false + + for _, component in ipairs(renderer) do + repeat + if component.enabled == false then + break + end + local component_data, component_wanted_width = + M.render_component(component, item, state, remaining_cols - (should_pad and 1 or 0)) + local actual_width = 0 + if component_data then + for _, data in ipairs(component_data) do + if data.text then + local padding = "" + if should_pad and #data.text and data.text:sub(1, 1) ~= " " and not data.no_padding then + padding = " " + end + data.text = padding .. data.text + should_pad = data.text:sub(#data.text) ~= " " and not data.no_next_padding + + actual_width = actual_width + vim.api.nvim_strwidth(data.text) + line:append(data.text, data.highlight) + remaining_cols = remaining_cols - vim.fn.strchars(data.text) + end + end + end + component_wanted_width = component_wanted_width or actual_width + wanted_width = wanted_width + component_wanted_width + until true + end + + line.wanted_width = wanted_width + if pre_render then + item.line = line + state.longest_node = math.max(state.longest_node, line.wanted_width) + else + item.line = nil + end + + return line +end + +---Sets the cursor at the specified node. +---@param state neotree.State The current state of the source. +---@param id string? The id of the node to set the cursor at. +---@return boolean boolean True if the node was found and focused, false +---otherwise. +M.focus_node = function(state, id, do_not_focus_window, relative_movement, bottom_scroll_padding) + if not id and not relative_movement then + log.debug("focus_node called with no id and no relative movement") + return false + end + relative_movement = relative_movement or 0 + bottom_scroll_padding = bottom_scroll_padding or 0 + + local tree = state.tree + if not tree then + log.debug("focus_node called with no tree") + return false + end + local node, linenr = tree:get_node(id) + if not node then + log.debug("focus_node cannot find node with id ", id) + return false + end + id = node:get_id() -- in case nil was passed in for id, meaning current node + + local bufnr = utils.get_value(state, "bufnr", 0, true) + if bufnr == 0 then + log.debug("focus_node: state has no bufnr ", state.bufnr, " / ", state.winid) + return false + end + if not vim.api.nvim_buf_is_valid(bufnr) then + log.debug("focus_node: bufnr is not valid") + return false + end + + if M.window_exists(state) then + if not linenr then + M.expand_to_node(state, node) + node, linenr = assert(tree:get_node(id)) + if not linenr then + log.debug("focus_node cannot get linenr for node with id ", id) + return false + end + end + local focus_window = not do_not_focus_window + if focus_window then + vim.api.nvim_set_current_win(state.winid) + end + + -- focus the correct line + linenr = linenr + relative_movement + local col = 0 + if node.indent then + col = string.len(node.indent) + end + local success, err = pcall(vim.api.nvim_win_set_cursor, state.winid, { linenr, col }) + + if success then + -- forget about cursor position as it is overwritten + M.position.clear(state) + -- now ensure that the window is scrolled correctly + local execute_win_command = function(cmd) + if vim.api.nvim_get_current_win() == state.winid then + vim.cmd(cmd) + else + vim.cmd("call win_execute(" .. state.winid .. [[, "]] .. cmd .. [[")]]) + end + end + + -- make sure we are not scrolled down if it can all fit on the screen + local lines = vim.api.nvim_buf_line_count(state.bufnr) + local win_height = vim.api.nvim_win_get_height(state.winid) + local virtual_bottom_line = vim.fn.line("w0", state.winid) + + win_height + - bottom_scroll_padding + if virtual_bottom_line <= linenr then + execute_win_command("normal! " .. (linenr + bottom_scroll_padding) .. "zb") + pcall(vim.api.nvim_win_set_cursor, state.winid, { linenr, col }) + elseif virtual_bottom_line > lines then + execute_win_command("normal! " .. (lines + bottom_scroll_padding) .. "zb") + pcall(vim.api.nvim_win_set_cursor, state.winid, { linenr, col }) + elseif linenr < (win_height / 2) then + execute_win_command("normal! zz") + end + else + log.debug("Failed to set cursor: " .. err) + end + return success + end + log.debug("focus_node: window does not exist") + return false +end + +M.get_all_visible_nodes = function(tree) + local nodes = {} + + local function process(node) + table.insert(nodes, node) + if node:is_expanded() then + if node:has_children() then + for _, child in ipairs(tree:get_nodes(node:get_id())) do + process(child) + end + end + end + end + + for _, node in ipairs(tree:get_nodes()) do + process(node) + end + return nodes +end + +M.get_expanded_nodes = function(tree, root_node_id) + local node_ids = {} + + local function process(node) + local id = node:get_id() + if node:is_expanded() then + table.insert(node_ids, id) + end + if node:has_children() then + for _, child in ipairs(tree:get_nodes(id)) do + process(child) + end + end + end + + if root_node_id then + local root_node = tree:get_node(root_node_id) + if root_node then + process(root_node) + end + else + for _, node in ipairs(tree:get_nodes()) do + process(node) + end + end + return node_ids +end + +M.collapse_all_nodes = function(tree, root_node_id) + local expanded = M.get_expanded_nodes(tree, root_node_id) + for _, id in ipairs(expanded) do + local node = tree:get_node(id) + if utils.is_expandable(node) then + node:collapse(id) + end + end + -- but make sure the root is expanded + local root = tree:get_nodes()[1] + if root then + root:expand() + end +end + +M.expand_to_node = function(state, node) + if not M.tree_is_visible(state) then + return + end + local tree = state.tree + if type(node) == "string" then + node = tree:get_node(node) + end + local parentId = node:get_parent_id() + while parentId do + local parent = tree:get_node(parentId) + parent:expand() + parentId = parent:get_parent_id() + end + render_tree(state) +end + +---Functions to save and restore the focused node. +M.position = {} +---@param state neotree.State +M.position.save = function(state) + if state.position.topline and state.position.lnum then + log.debug("There's already a position saved to be restored. Cannot save another.") + return + end + if state.tree and M.window_exists(state) then + local win_state = vim.api.nvim_win_call(state.winid, vim.fn.winsaveview) + state.position.topline = win_state.topline + state.position.lnum = win_state.lnum + log.debug("Saved cursor position with lnum: " .. state.position.lnum) + log.debug("Saved window position with topline: " .. state.position.topline) + end +end +---@param state neotree.State +M.position.set = function(state, node_id) + if type(node_id) ~= "string" or node_id == "" then + return + end + if state.tree then + local node = state.tree:get_node(node_id) + if node and node.skip_node then + return + end + end + state.position.node_id = node_id +end +---@param state neotree.State +M.position.clear = function(state) + log.debug("Forget about cursor position.") + -- Clear saved position, so that we can save another position later. + state.position.topline = nil + state.position.lnum = nil + -- After focusing a node, we clear it so that subsequent renderer.position.restore don't + -- focus on it anymore + state.position.node_id = nil +end +---@param state neotree.State +M.position.restore = function(state) + if state.position.topline and state.position.lnum then + log.debug("Restoring window position to topline: " .. state.position.topline) + log.debug("Restoring cursor position to lnum: " .. state.position.lnum) + vim.api.nvim_win_call(state.winid, function() + vim.fn.winrestview({ topline = state.position.topline, lnum = state.position.lnum }) + end) + end + if state.position.node_id then + log.debug("Focusing on node_id: " .. state.position.node_id) + M.focus_node(state, state.position.node_id, true) + end + M.position.clear(state) +end + +---Redraw the tree without relaoding from the source. +---@param state neotree.State State of the tree. +M.redraw = function(state) + if state.tree and M.tree_is_visible(state) then + log.trace("Redrawing tree", state.name, state.id) + -- every now and then this will fail because the window was closed in + -- betweeen the start of an async refresh and the redraw call. + -- This is not a problem, so we just ignore the error. + local success = pcall(render_tree, state) + if success then + log.trace(" Redrawing tree done", state.name, state.id) + else + log.trace(" Redrawing tree failed, maybe it was closed?", state.name, state.id) + end + end +end +---Visit all nodes ina tree recursively and reduce to a single value. +---@param tree table NuiTree +---@param memo any Value that is passed to the accumulator function +---@param func function Accumulator function that is called for each node +---@return any any The final memo value. +M.reduce_nodes = function(tree, memo, func) + if type(func) ~= "function" then + error("func must be a function") + end + local visit + visit = function(node) + func(node, memo) + if node:has_children() then + for _, child in ipairs(tree:get_nodes(node:get_id())) do + visit(child) + end + end + end + for _, node in ipairs(tree:get_nodes()) do + visit(node) + end + return memo +end + +---Visits all nodes in the tree and returns a list of all nodes that match the +---given predicate. +---@param tree table The NuiTree to search. +---@param selector_func function The predicate function, should return true for +---nodes that should be included in the result. +---@return table table A list of nodes that match the predicate. +M.select_nodes = function(tree, selector_func, limit) + if type(selector_func) ~= "function" then + error("selector_func must be a function") + end + local found_nodes = {} + local visit + visit = function(node) + if selector_func(node) then + table.insert(found_nodes, node) + if limit and #found_nodes >= limit then + return + end + end + if node:has_children() then + for _, child in ipairs(tree:get_nodes(node:get_id())) do + visit(child) + end + end + end + for _, node in ipairs(tree:get_nodes()) do + visit(node) + if limit and #found_nodes >= limit then + break + end + end + return found_nodes +end + +M.set_expanded_nodes = function(tree, expanded_nodes) + M.collapse_all_nodes(tree) + log.debug("Setting expanded nodes") + for _, id in ipairs(expanded_nodes or {}) do + local node = tree:get_node(id) + if node ~= nil then + node:expand() + end + end +end + +create_tree = function(state) + if state.tree and state.tree.bufnr == state.bufnr then + if buffer_is_usable(state.tree.bufnr) then + log.debug("Tree already exists and buffer is valid, skipping creation", state.name, state.id) + state.tree.winid = state.winid + return + end + end + state.tree = NuiTree({ + ns_id = highlights.ns_id, + winid = state.winid, + bufnr = state.bufnr, + get_node_id = function(node) + return node.id + end, + prepare_node = function(data) + return prepare_node(data, state) + end, + }) +end + +---@return NuiTree.Node[]? +local get_selected_nodes = function(state) + if state.winid ~= vim.api.nvim_get_current_win() then + return nil + end + local start_pos = vim.fn.getpos("'<")[2] + local end_pos = vim.fn.getpos("'>")[2] + if end_pos < start_pos then + -- I'm not sure if this could actually happen, but just in case + start_pos, end_pos = end_pos, start_pos + end + local selected_nodes = {} + while start_pos <= end_pos do + local node = state.tree:get_node(start_pos) + if node then + table.insert(selected_nodes, node) + end + start_pos = start_pos + 1 + end + return selected_nodes +end + +---@class neotree.State.ResolvedMapping +---@field text string +---@field handler function + +---@param state neotree.StateWithTree +local set_buffer_mappings = function(state) + ---@type table + local resolved_mappings = {} + local skip_this_mapping = { + ["none"] = true, + ["nop"] = true, + ["noop"] = true, + } + local mappings = state.window.mappings or {} + local mapping_options = state.window.mapping_options or { noremap = true } + for cmd, func in pairs(mappings) do + ---@type neotree.TreeCommandVisual? + local vfunc + local config = {} + repeat + if not utils.truthy(func) then + break + end + if skip_this_mapping[func] then + log.trace("Skipping mapping for %s", cmd) + break + end + local oldfunc = func + local map_options = vim.deepcopy(mapping_options) + local desc + if type(func) == "table" then + ---@cast func neotree.Config.Window.Command.Configured + for key, value in pairs(func) do + if key ~= "command" and key ~= 1 and key ~= "config" then + map_options[key] = value + end + end + desc = func.desc + config = func.config or {} + func = func.command or func[1] + end + + ---@type string? + local helptext + if type(func) == "string" then + helptext = desc or func + map_options.desc = map_options.desc or func + vfunc = state.commands[func .. "_visual"] + func = state.commands[func] + elseif type(func) == "function" then + ---@cast func neotree.TreeCommand + helptext = desc or "" + else + error("mapping needs to be either a function or a valid name of a command") + end + + if type(func) ~= "function" then + local invalid_desc = desc or func or oldfunc + log.warn("Invalid mapping for ", cmd, ": ", invalid_desc) + resolved_mappings[cmd] = { + text = (" (%s)"):format(invalid_desc), + handler = function() end, + } + break + end + local fallback = utils.keycode(cmd) + resolved_mappings[cmd] = { + text = helptext, + handler = function() + state.config = config + state.fallback = fallback + return func(state) + end, + } + keymap.set(state.bufnr, "n", cmd, resolved_mappings[cmd].handler, map_options) + if type(vfunc) == "function" then + keymap.set(state.bufnr, "v", cmd, function() + vim.api.nvim_feedkeys(ESC_KEY, "i", true) + vim.schedule(function() + local selected_nodes = get_selected_nodes(state) + if utils.truthy(selected_nodes) then + ---@cast selected_nodes -nil + state.config = config + vfunc(state, selected_nodes) + end + end) + end, map_options) + end + until true + end + state.resolved_mappings = resolved_mappings +end + +local function create_floating_window(state, win_options, bufname) + state.force_float = nil + -- First get the default options for floating windows. + local title = utils.resolve_config_option(state, "window.popup.title", function(current_state) + return "Neo-tree " .. current_state.name:gsub("^%l", string.upper) + end) + win_options = popups.popup_options(title, 40, win_options) + win_options.win_options = nil + win_options.zindex = 40 + + -- Then override with source specific options. + local b = win_options.border + win_options.size = utils.resolve_config_option(state, "window.popup.size", default_popup_size) + win_options.position = utils.resolve_config_option(state, "window.popup.position", "50%") + win_options.border = utils.resolve_config_option(state, "window.popup.border", b) + + ---@class NuiPopup + local win = NuiPopup(win_options) + win:mount() + win.source_name = state.name + win.original_options = state.window + table.insert(floating_windows, win) + + win:on({ "BufHidden" }, function() + vim.schedule(function() + win:unmount() + end) + end, { once = true }) + state.winid = win.winid + state.bufnr = win.bufnr + log.debug("Created floating window with winid: ", win.winid, " and bufnr: ", win.bufnr) + vim.api.nvim_buf_set_name(state.bufnr, bufname) + + -- why is this necessary? + vim.api.nvim_set_current_win(win.winid) + return win +end + +local get_buffer = function(bufname, state) + local bufnr = vim.fn.bufnr(bufname) + if bufnr > 0 then + if vim.api.nvim_buf_is_valid(bufnr) and vim.api.nvim_buf_is_loaded(bufnr) then + return bufnr + else + pcall(vim.api.nvim_buf_delete, bufnr, { force = true }) + bufnr = 0 + end + end + if bufnr < 1 then + bufnr = vim.api.nvim_create_buf(false, false) + vim.api.nvim_buf_set_name(bufnr, bufname) + vim.bo[bufnr].buftype = "nofile" + vim.bo[bufnr].swapfile = false + vim.bo[bufnr].filetype = "neo-tree" + vim.bo[bufnr].modifiable = false + vim.bo[bufnr].undolevels = -1 + autocmd.buf.define(bufnr, "BufDelete", function() + M.position.save(state) + end) + end + return bufnr +end + +M.acquire_window = function(state) + if M.window_exists(state) then + return state.winid + end + + -- used by tests to determine if the tree is ready for testing + state._ready = false + + local default_position = utils.resolve_config_option(state, "window.position", "left") + local relative = utils.resolve_config_option(state, "window.relative", "editor") + state.current_position = state.current_position or default_position + + local bufname = string.format("neo-tree %s [%s]", state.name, state.id) + local size_opt, default_size + if state.current_position == "top" or state.current_position == "bottom" then + size_opt, default_size = "window.height", "15" + else + size_opt, default_size = "window.width", "40" + end + local win_options = { + ns_id = highlights.ns_id, + size = utils.resolve_config_option(state, size_opt, default_size), + position = state.current_position, + relative = relative, + buf_options = { + buftype = "nofile", + modifiable = false, + swapfile = false, + filetype = "neo-tree", + undolevels = -1, + }, + win_options = { + colorcolumn = "", + signcolumn = "no", + }, + } + + local event_args = { + position = state.current_position, + source = state.name, + tabnr = tabid_to_tabnr(state.tabid), -- for compatibility + tabid = state.tabid, + } + events.fire_event(events.NEO_TREE_WINDOW_BEFORE_OPEN, event_args) + + local win + if state.current_position == "float" then + M.close_all_floating_windows() + M.close(state) + win = create_floating_window(state, win_options, bufname) + elseif state.current_position == "current" then + -- state.id is always the window id or tabnr that this state was created for + -- in the case of a position = current state object, it will be the window id + local winid = state.id + if not vim.api.nvim_win_is_valid(winid) then + log.warn("Window ", winid, " is no longer valid!") + return + end + state.winid = winid + state.bufnr = get_buffer(bufname, state) + vim.api.nvim_win_set_buf(state.winid, state.bufnr) + else + local close_old_window = function(new_winid) + if state.winid and new_winid ~= state.winid then + -- This state may be showing in another window, close it first because + -- each state can only be shown in one window at a time. + M.close(state, false) + end + end + local location = windows.get_location(state.current_position) + if location.winid > 0 then + close_old_window(location.winid) + state.bufnr = get_buffer(bufname, state) + state.winid = location.winid + vim.api.nvim_win_set_buf(state.winid, state.bufnr) + else + close_old_window() + win = NuiSplit(win_options) + win:mount() + state.bufnr = win.bufnr + state.winid = win.winid + location.winid = state.winid + end + location.source = state.name + end + event_args.winid = state.winid + events.fire_event(events.NEO_TREE_WINDOW_AFTER_OPEN, event_args) + + if type(state.bufnr) == "number" then + vim.api.nvim_buf_set_var(state.bufnr, "neo_tree_source", state.name) + vim.api.nvim_buf_set_var(state.bufnr, "neo_tree_tabnr", tabid_to_tabnr(state.tabid)) + vim.api.nvim_buf_set_var(state.bufnr, "neo_tree_tabid", state.tabid) + vim.api.nvim_buf_set_var(state.bufnr, "neo_tree_position", state.current_position) + vim.api.nvim_buf_set_var(state.bufnr, "neo_tree_winid", state.winid) + end + + if win ~= nil then + vim.api.nvim_buf_set_name(state.bufnr, bufname) + vim.api.nvim_set_current_win(state.winid) + -- Used to track the position of the cursor within the tree as it gains and loses focus + win:on({ "BufDelete" }, function() + M.position.save(state) + end) + win:on({ "BufDelete" }, function() + vim.schedule(function() + win:unmount() + end) + end, { once = true }) + end + + set_buffer_mappings(state) + return state.winid +end + +M.update_floating_window_layouts = function() + update_floating_windows() + for _, win in ipairs(floating_windows) do + local opt = { + relative = "win", + } + opt.size = utils.resolve_config_option(win.original_options, "popup.size", default_popup_size) + opt.position = utils.resolve_config_option(win.original_options, "popup.position", "50%") + win:update_layout(opt) + end +end + +---Determines is the givin winid is valid and the window still exists. +---@param winid any +---@return boolean +M.is_window_valid = function(winid) + if winid == nil then + return false + end + if type(winid) == "number" and winid > 0 then + return vim.api.nvim_win_is_valid(winid) + else + return false + end +end + +---Determines if the window exists and is valid. +---@param state neotree.State The current state of the plugin. +---@return boolean True if the window exists and is valid, false otherwise. +M.window_exists = function(state) + local window_exists + local winid = utils.get_value(state, "winid", 0, true) + local bufnr = utils.get_value(state, "bufnr", 0, true) + local default_position = utils.get_value(state, "window.position", "left", true) + local position = state.current_position or default_position + + if winid == 0 then + window_exists = false + elseif position == "current" then + window_exists = vim.api.nvim_win_is_valid(winid) + and vim.api.nvim_buf_is_loaded(bufnr) + and vim.api.nvim_win_get_buf(winid) == bufnr + else + local isvalid = M.is_window_valid(winid) + window_exists = isvalid and (vim.api.nvim_win_get_number(winid) > 0) + if window_exists then + local winbufnr = vim.api.nvim_win_get_buf(winid) + if winbufnr < 1 then + return false + else + if winbufnr ~= bufnr then + return false + end + local success, buf_position = pcall(vim.api.nvim_buf_get_var, bufnr, "neo_tree_position") + if not success then + return false + end + if buf_position ~= position then + return false + end + end + end + end + return window_exists +end + +---Determines if a specific tree is open. +---@param state neotree.State The current state of the plugin. +---@return boolean +M.tree_is_visible = function(state) + return M.window_exists(state) and vim.api.nvim_win_get_buf(state.winid) == state.bufnr +end + +---Renders the given tree and expands window width if needed +--@param state neotree.State The state containing tree to render. Almost same as state.tree:render() +render_tree = function(state) + local add_blank_line_at_top = require("neo-tree").config.add_blank_line_at_top + local should_auto_expand = state.window.auto_expand_width and state.current_position ~= "float" + local should_pre_render = should_auto_expand or state.current_position == "current" + + log.debug("render_tree: Saving position") + M.position.save(state) + + if should_pre_render and M.tree_is_visible(state) then + log.trace("pre-rendering tree") + state._in_pre_render = true + if add_blank_line_at_top then + state.tree:render(2) + else + state.tree:render() + end + state._in_pre_render = false + local textoffset = vim.fn.getwininfo(state.winid)[1].textoff or 0 + local desired_width = state.longest_node + textoffset + state.window.last_user_width = vim.api.nvim_win_get_width(state.winid) + if should_auto_expand and desired_width > state.window.last_user_width then + log.trace(string.format("auto_expand_width: on. Expanding width to %s.", state.longest_node)) + vim.api.nvim_win_set_width(state.winid, desired_width) + state.win_width = desired_width + end + end + if M.tree_is_visible(state) then + if add_blank_line_at_top then + state.tree:render(2) + else + state.tree:render() + end + end + + log.debug("render_tree: Restoring position") + M.position.restore(state) +end + +---Draws the given nodes on the screen. +---@param nodes NuiTree.Node[] The nodes to draw. +---@param state neotree.State The current state of the source. +draw = function(nodes, state, parent_id) + -- If we are going to redraw, preserve the current set of expanded nodes. + local expanded_nodes = {} + if parent_id == nil and state.tree ~= nil then + if state.force_open_folders then + log.trace("Force open folders") + state.force_open_folders = nil + else + log.trace("Preserving expanded nodes") + expanded_nodes = M.get_expanded_nodes(state.tree) + end + end + if state.default_expanded_nodes then + for _, id in ipairs(state.default_expanded_nodes) do + table.insert(expanded_nodes, id) + end + end + + -- Create the tree if it doesn't exist. + if parent_id then + if not M.window_exists(state) then + log.trace("Window is gone, aborting lazy load of folder") + return + end + else + M.acquire_window(state) + create_tree(state) + end + + -- draw the given nodes + local success, msg = pcall(state.tree.set_nodes, state.tree, nodes, parent_id) + if not success then + log.error("Error setting nodes: ", msg) + log.error(vim.inspect(state.tree:get_nodes())) + end + if parent_id ~= nil then + -- this is a dynamic fetch of children that were not previously loaded + local node = assert(state.tree:get_node(parent_id)) + node.loaded = true + node:expand() + else + M.set_expanded_nodes(state.tree, expanded_nodes) + end + + -- This is to ensure that containers are always the right size + state.win_width = utils.get_inner_win_width(state.winid) + start_resize_monitor() + + render_tree(state) + + -- draw winbar / statusbar + require("neo-tree.ui.selector").set_source_selector(state) + + state._ready = true +end + +local function group_empty_dirs(node) + if node.children == nil then + return node + end + + local first_child = node.children[1] + if #node.children == 1 and first_child.type == "directory" then + -- this is the only path that changes the tree + -- at each step where we discover an empty directory, merge it's name with the parent + -- then skip over it + first_child.name = node.name .. utils.path_separator .. first_child.name + return group_empty_dirs(first_child) + else + for i, child in ipairs(node.children) do + node.children[i] = group_empty_dirs(child) + end + return node + end +end + +---Shows the given items as a tree. +---@param sourceItems table? The list of items to transform. +---@param state neotree.State The current state of the plugin. +---@param parentId string? The id of the parent node to display these nodes at +---@param callback function? The id of the parent node to display these nodes at +M.show_nodes = function(sourceItems, state, parentId, callback) + --local id = string.format("show_nodes %s:%s [%s]", state.name, state.force_float, state.tabid) + --utils.debounce(id, function() + if not sourceItems then + return + end + events.fire_event(events.BEFORE_RENDER, state) + state.longest_width_exact = 0 + local parent + local level = 0 + if parentId ~= nil then + local success + success, parent = pcall(state.tree.get_node, state.tree, parentId) + if success and parent then + level = parent:get_depth() + end + state.longest_node = state.longest_node or 0 + else + state.longest_node = 0 + end + + local config = require("neo-tree").config + if config.hide_root_node then + if not parentId then + sourceItems[1].skip_node = true + if not (sourceItems[1].children and #sourceItems[1].children > 0) then + sourceItems[1].is_empty_with_hidden_root = true + end + end + if not config.retain_hidden_root_indent then + level = level - 1 + end + end + + if state.group_empty_dirs then + if parent then + local scan_mode = require("neo-tree").config.filesystem.scan_mode + if scan_mode == "deep" then + for i, item in ipairs(sourceItems) do + sourceItems[i] = group_empty_dirs(item) + end + else + -- this is a lazy load of a single sub folder + group_empty_dirs(sourceItems) + if #sourceItems == 1 and sourceItems[1].type == "directory" then + -- This folder needs to be grouped. + -- The goal is to just update the existing node in place. + -- To avoid digging into private internals of Nui, we will just export the entire level and replace + -- the one node. This keeps it in the right order, because nui doesn't have methods to replace something + -- in place. + -- We can't just mutate the existing node because we have to change it's id which would break Nui's + -- internal state. + local item = sourceItems[1] + parentId = parent:get_parent_id() + local siblings = state.tree:get_nodes(parentId) + for i, node in pairs(siblings) do + if node.id == parent.id then + item.name = parent.name .. utils.path_separator .. item.name + item.level = level - 1 + item.loaded = utils.truthy(item.children) + siblings[i] = NuiTree.Node(item, item.children) + break + end + end + sourceItems = nil -- this is a signal to skip the rest of the processing + state.tree:set_nodes(siblings, parentId) + end + end + else + -- if we are rendering a whole tree, just group the children because we don'the + -- want to change the root nodes + for _, item in ipairs(sourceItems) do + if item.children ~= nil then + for i, child in ipairs(item.children) do + item.children[i] = group_empty_dirs(child) + end + end + end + end + end + + if sourceItems then + -- normal path + local nodes = create_nodes(sourceItems, state, level) + draw(nodes, state, parentId) + else + -- this was a force grouping of a lazy loaded folder + state.win_width = utils.get_inner_win_width(state.winid) + render_tree(state) + end + + vim.schedule(function() + events.fire_event(events.AFTER_RENDER, state) + end) + if type(callback) == "function" then + vim.schedule(callback) + end + --end, 100) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/selector.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/selector.lua new file mode 100644 index 0000000..6a501c7 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/selector.lua @@ -0,0 +1,405 @@ +local utils = require("neo-tree.utils") +local log = require("neo-tree.log") +local manager = require("neo-tree.sources.manager") + +local M = {} + +---calc_click_id_from_source: +-- Calculates click_id that stores information of the source and window id +-- DANGER: Do not change this function unless you know what you are doing +---@param winid integer: window id of the window source_selector is placed +---@param source_index integer: index of the source +---@return integer +local calc_click_id_from_source = function(winid, source_index) + local base_number = #require("neo-tree").config.source_selector.sources + 1 + return base_number * winid + source_index +end + +---calc_source_from_click_id: +-- Calculates source index and window id from click_id. Paired with `M.calc_click_id_from_source` +-- DANGER: Do not change this function unless you know what you are doing +---@param click_id integer: click_id +---@return integer, integer +local calc_source_from_click_id = function(click_id) + local base_number = #require("neo-tree").config.source_selector.sources + 1 + return math.floor(click_id / base_number), click_id % base_number +end +---sep_tbl: +-- Returns table expression of separator. +-- Converts to table expression if sep is string. +---@param sep string | table: +---@return table: `{ left = .., right = .., override = .. }` +local sep_tbl = function(sep) + if type(sep) == "nil" then + return {} + elseif type(sep) ~= "table" then + return { left = sep, right = sep, override = "active" } + end + return sep +end + +---get_separators +-- Returns information about separator on each tab. +---@param source_index integer: index of source +---@param active_index integer: index of active source. used to check if source is active and when `override = "active"` +---@param force_ignore_left boolean: overwrites calculated results with "" if set to true +---@param force_ignore_right boolean: overwrites calculated results with "" if set to true +---@return table: something like `{ left = "|", right = "|" }` +local get_separators = function(source_index, active_index, force_ignore_left, force_ignore_right) + local config = require("neo-tree").config + local is_active = source_index == active_index + local sep = sep_tbl(config.source_selector.separator) + if is_active then + sep = vim.tbl_deep_extend("force", sep, sep_tbl(config.source_selector.separator_active)) + end + local show_left = sep.override == "left" + or (sep.override == "active" and source_index <= active_index) + or sep.override == nil + local show_right = sep.override == "right" + or (sep.override == "active" and source_index >= active_index) + or sep.override == nil + return { + left = (show_left and not force_ignore_left) and sep.left or "", + right = (show_right and not force_ignore_right) and sep.right or "", + } +end + +---get_selector_tab_info: +-- Returns information to create a tab +---@param source_name string: name of source. should be same as names in `config.sources` +---@param source_index integer: index of source_name +---@param is_active boolean: whether this source is currently focused +---@param separator table: `{ left = .., right = .. }`: output from `get_separators()` +---@return table (see code): Note: `length`: length of whole tab (including seps), `text_length`: length of tab excluding seps +local get_selector_tab_info = function(source_name, source_index, is_active, separator) + local config = require("neo-tree").config + local separator_config = utils.resolve_config_option(config, "source_selector", nil) + if separator_config == nil then + log.warn("Cannot find source_selector config. `get_selector` abort.") + return {} + end + local source_config = config[source_name] or {} + local get_strlen = vim.api.nvim_strwidth + local text = separator_config.sources[source_index].display_name + or source_config.display_name + or source_name + local text_length = get_strlen(text) + if separator_config.tabs_min_width ~= nil and text_length < separator_config.tabs_min_width then + text = M.text_layout(text, separator_config.content_layout, separator_config.tabs_min_width) + text_length = separator_config.tabs_min_width + end + if separator_config.tabs_max_width ~= nil and text_length > separator_config.tabs_max_width then + text = M.text_layout(text, separator_config.content_layout, separator_config.tabs_max_width) + text_length = separator_config.tabs_max_width + end + local tab_hl = is_active and separator_config.highlight_tab_active + or separator_config.highlight_tab + local sep_hl = is_active and separator_config.highlight_separator_active + or separator_config.highlight_separator + return { + index = source_index, + is_active = is_active, + left = separator.left, + right = separator.right, + text = text, + tab_hl = tab_hl, + sep_hl = sep_hl, + length = text_length + get_strlen(separator.left) + get_strlen(separator.right), + text_length = text_length, + } +end + +---text_with_hl: +-- Returns text with highlight syntax for winbar / statusline +---@param text string: text to highlight +---@param tab_hl string | nil: if nil, does nothing +---@return string: e.g. "%#HiName#text" +local text_with_hl = function(text, tab_hl) + if tab_hl == nil then + return text + end + return string.format("%%#%s#%s", tab_hl, text) +end + +---add_padding: +-- Use for creating padding with highlight +---@param padding_legth number: number of padding. if float, value is rounded with `math.floor` +---@param padchar string | nil: if nil, " " (space) is used +---@return string +local add_padding = function(padding_legth, padchar) + if padchar == nil then + padchar = " " + end + return string.rep(padchar, math.floor(padding_legth)) +end + +---text_layout: +-- Add padding to fill `output_width`. +-- If `output_width` is less than `text_length`, text is truncated to fit `output_width`. +---@param text string: +---@param content_layout string: `"start", "center", "end"`: see `config.source_selector.tabs_layout` for more details +---@param output_width integer: exact `strdisplaywidth` of the output string +---@param trunc_char string | nil: Character used to indicate truncation. If nil, "…" (ellipsis) is used. +---@return string +local text_layout = function(text, content_layout, output_width, trunc_char) + if output_width < 1 then + return "" + end + local text_length = vim.fn.strdisplaywidth(text) + local pad_length = output_width - text_length + local left_pad, right_pad = 0, 0 + if pad_length < 0 then + if output_width < 4 then + return (utils.truncate_by_cell(text, output_width)) + else + return (utils.truncate_by_cell(text, output_width - 1) .. trunc_char) + end + elseif content_layout == "start" then + left_pad, right_pad = 0, pad_length + elseif content_layout == "end" then + left_pad, right_pad = pad_length, 0 + elseif content_layout == "center" then + left_pad, right_pad = pad_length / 2, math.ceil(pad_length / 2) + end + return add_padding(left_pad) .. text .. add_padding(right_pad) +end + +---render_tab: +-- Renders string to express one tab for winbar / statusline. +---@param left_sep string: left separator +---@param right_sep string: right separator +---@param sep_hl string: highlight of separators +---@param text string: text, mostly name of source in this case +---@param tab_hl string: highlight of text +---@param click_id integer: id passed to `___neotree_selector_click`, should be calculated with `M.calc_click_id_from_source` +---@return string: complete string to render one tab +local render_tab = function(left_sep, right_sep, sep_hl, text, tab_hl, click_id) + local res = "%" .. click_id .. "@v:lua.___neotree_selector_click@" + if left_sep ~= nil then + res = res .. text_with_hl(left_sep, sep_hl) + end + res = res .. text_with_hl(text, tab_hl) + if right_sep ~= nil then + res = res .. text_with_hl(right_sep, sep_hl) + end + return res +end + +M.get_scrolled_off_node_text = function(state) + if state == nil then + state = require("neo-tree.sources.manager").get_state_for_window() + if state == nil then + return + end + end + local win_top_line = vim.fn.line("w0") + if win_top_line == nil or win_top_line == 1 then + return + end + local node = assert(state.tree:get_node(win_top_line)) + return "  " .. vim.fn.fnamemodify(node.path, ":~:h") +end + +M.get = function() + local state = require("neo-tree.sources.manager").get_state_for_window() + if state == nil then + return + else + local config = require("neo-tree").config + local scrolled_off = + utils.resolve_config_option(config, "source_selector.show_scrolled_off_parent_node", false) + if scrolled_off then + local node_text = M.get_scrolled_off_node_text(state) + if node_text ~= nil then + return node_text + end + end + return M.get_selector(state, vim.api.nvim_win_get_width(0)) + end +end + +---get_selector: +-- Does everything to generate the string for source_selector in winbar / statusline. +---@param state neotree.State: +---@param width integer: width of the entire window where the source_selector is displayed +---@return string | nil +M.get_selector = function(state, width) + local config = require("neo-tree").config + if config == nil then + log.warn("Cannot find config. `get_selector` abort.") + return nil + end + local winid = state.winid or vim.api.nvim_get_current_win() + + -- load padding from config + local padding_config = config.source_selector.padding + local padding + if type(padding_config) == "number" then + padding = { left = padding_config, right = padding_config } + else + padding = padding_config or { left = 0, right = 0 } + end + width = math.floor(width - padding.left - padding.right) + + -- generate information of each tab (look `get_selector_tab_info` for type hint) + local tabs = {} + local sources = config.source_selector.sources or {} + local active_index = #sources + local length_sum, length_active, length_separators = 0, 0, 0 + for i, source_info in ipairs(sources) do + local is_active = source_info.source == state.name + if is_active then + active_index = i + end + local separator = get_separators( + i, + active_index, + config.source_selector.show_separator_on_edge == false and i == 1, + config.source_selector.show_separator_on_edge == false and i == #sources + ) + local element = get_selector_tab_info(source_info.source, i, is_active, separator) + length_sum = length_sum + element.length + length_separators = length_separators + element.length - element.text_length + if is_active then + length_active = element.length + end + table.insert(tabs, element) + end + + -- start creating string to display + local tabs_layout = config.source_selector.tabs_layout or "equal" + local content_layout = config.source_selector.content_layout or "center" + local hl_background = config.source_selector.highlight_background + local trunc_char = config.source_selector.truncation_character or "…" + local remaining_width = width - length_separators + local return_string = text_with_hl(add_padding(padding.left), hl_background) + if width < length_sum then -- not enough width + tabs_layout = "equal" -- other methods cannot handle this + end + if tabs_layout == "active" then + local active_tab_length = width - length_sum + length_active - 1 + for _, tab in ipairs(tabs) do + return_string = return_string + .. render_tab( + tab.left, + tab.right, + tab.sep_hl, + text_layout( + tab.text, + tab.is_active and content_layout or nil, + active_tab_length, + trunc_char + ), + tab.tab_hl, + calc_click_id_from_source(winid, tab.index) + ) + .. text_with_hl("", hl_background) + end + elseif tabs_layout == "equal" then + for _, tab in ipairs(tabs) do + return_string = return_string + .. render_tab( + tab.left, + tab.right, + tab.sep_hl, + text_layout(tab.text, content_layout, math.floor(remaining_width / #tabs), trunc_char), + tab.tab_hl, + calc_click_id_from_source(winid, tab.index) + ) + .. text_with_hl("", hl_background) + end + else -- config.source_selector.tab_labels == "start", "end", "center" + -- calculate padding based on tabs_layout + local pad_length = width - length_sum + local left_pad, right_pad = 0, 0 + if pad_length > 0 then + if tabs_layout == "start" then + left_pad, right_pad = 0, pad_length + elseif tabs_layout == "end" then + left_pad, right_pad = pad_length, 0 + elseif tabs_layout == "center" then + left_pad, right_pad = pad_length / 2, math.ceil(pad_length / 2) + end + end + + for i, tab in ipairs(tabs) do + if width == 0 then + break + end + + -- only render trunc_char if there is no space for the tab + local sep_length = tab.length - tab.text_length + if width <= sep_length + 1 then + return_string = return_string + .. text_with_hl(trunc_char .. add_padding(width - 1), hl_background) + width = 0 + break + end + + -- tab_length should not exceed width + local tab_length = width < tab.length and width or tab.length + width = width - tab_length + + -- add padding for first and last tab + local tab_text = tab.text + if i == 1 then + tab_text = add_padding(left_pad) .. tab_text + tab_length = tab_length + left_pad + end + if i == #tabs then + tab_text = tab_text .. add_padding(right_pad) + tab_length = tab_length + right_pad + end + + return_string = return_string + .. render_tab( + tab.left, + tab.right, + tab.sep_hl, + text_layout(tab_text, tabs_layout, tab_length - sep_length, trunc_char), + tab.tab_hl, + calc_click_id_from_source(winid, tab.index) + ) + end + end + return return_string .. "%<%0@v:lua.___neotree_selector_click@" +end + +---set_source_selector: +-- (public): Directly set source_selector to current window's winbar / statusline +---@param state neotree.State: state +---@return nil +M.set_source_selector = function(state) + if state.enable_source_selector == false then + return + end + local sel_config = utils.resolve_config_option(require("neo-tree").config, "source_selector", {}) + if sel_config and sel_config.winbar then + vim.wo[state.winid].winbar = "%{%v:lua.require'neo-tree.ui.selector'.get()%}" + end + if sel_config and sel_config.statusline then + vim.wo[state.winid].statusline = "%{%v:lua.require'neo-tree.ui.selector'.get()%}" + end +end + +-- @v:lua@ in the tabline only supports global functions, so this is +-- the only way to add click handlers without autoloaded vimscript functions +_G.___neotree_selector_click = function(id, _, _, _) + if id < 1 then + return + end + local sources = require("neo-tree").config.source_selector.sources or {} + local winid, source_index = calc_source_from_click_id(id) + local state = manager.get_state_for_window(winid) + if state == nil then + log.warn("state not found for window ", winid, "; ignoring click") + return + end + require("neo-tree.command").execute({ + source = sources[source_index].source, + position = state.current_position, + action = "focus", + }) +end + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/windows.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/windows.lua new file mode 100644 index 0000000..1c626c5 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/ui/windows.lua @@ -0,0 +1,27 @@ +local locations = {} + +locations.get_location = function(location) + local tab = vim.api.nvim_get_current_tabpage() + if not locations[tab] then + locations[tab] = {} + end + local loc = locations[tab][location] + if loc then + if loc.winid ~= 0 then + -- verify the window before we return it + if not vim.api.nvim_win_is_valid(loc.winid) then + loc.winid = 0 + end + end + return loc + end + loc = { + source = nil, + name = location, + winid = 0, + } + locations[tab][location] = loc + return loc +end + +return locations diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/utils/_compat.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/utils/_compat.lua new file mode 100644 index 0000000..bf14884 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/utils/_compat.lua @@ -0,0 +1,42 @@ +local compat = {} +---@return boolean +compat.noref = function() + return vim.fn.has("nvim-0.10") == 1 and true or {} --[[@as boolean]] +end + +---source: https://github.com/Validark/Lua-table-functions/blob/master/table.lua +---Moves elements [f, e] from array a1 into a2 starting at index t +---table.move implementation +---@generic T: table +---@param a1 T from which to draw elements from range +---@param f integer starting index for range +---@param e integer ending index for range +---@param t integer starting index to move elements from a1 within [f, e] +---@param a2 T the second table to move these elements to +---@default a2 = a1 +---@returns a2 +local table_move = function(a1, f, e, t, a2) + a2 = a2 or a1 + t = t + e + + for i = e, f, -1 do + t = t - 1 + a2[t] = a1[i] + end + + return a2 +end +---source: +compat.table_move = table.move or table_move + +---@vararg any +local table_pack = function(...) + -- Returns a new table with parameters stored into an array, with field "n" being the total number of parameters + local t = { ... } + ---@diagnostic disable-next-line: inject-field + t.n = #t + return t +end +compat.table_pack = table.pack or table_pack + +return compat diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/utils/filesize/LICENSE b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/utils/filesize/LICENSE new file mode 100644 index 0000000..18580e0 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/utils/filesize/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Boris Nagaev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/utils/filesize/filesize.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/utils/filesize/filesize.lua new file mode 100644 index 0000000..3e3ff13 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/utils/filesize/filesize.lua @@ -0,0 +1,134 @@ +-- lua-filesize, generate a human readable string describing the file size +-- Copyright (c) 2016 Boris Nagaev +-- See the LICENSE file for terms of use. + +local si = { + bits = { "b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb" }, + bytes = { "B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }, +} + +local function isNan(num) + -- http://lua-users.org/wiki/InfAndNanComparisons + -- NaN is the only value that doesn't equal itself + return num ~= num +end + +local function roundNumber(num, digits) + local fmt = "%." .. digits .. "f" + return tonumber(fmt:format(num)) +end + +local function filesize(size, options) + -- copy options to o + local o = {} + for key, value in pairs(options or {}) do + o[key] = value + end + + local function setDefault(name, default) + if o[name] == nil then + o[name] = default + end + end + setDefault("bits", false) + setDefault("unix", false) + setDefault("base", 2) + setDefault("round", o.unix and 1 or 2) + setDefault("spacer", o.unix and "" or " ") + setDefault("suffixes", {}) + setDefault("output", "string") + setDefault("exponent", -1) + + assert(not isNan(size), "Invalid arguments") + + local ceil = (o.base > 2) and 1000 or 1024 + local negative = (size < 0) + if negative then + -- Flipping a negative number to determine the size + size = -size + end + + local result + + -- Zero is now a special case because bytes divide by 1 + if size == 0 then + result = { + 0, + o.unix and "" or (o.bits and "b" or "B"), + } + else + -- Determining the exponent + if o.exponent == -1 or isNan(o.exponent) then + o.exponent = math.floor(math.log(size) / math.log(ceil)) + end + + -- Exceeding supported length, time to reduce & multiply + if o.exponent > 8 then + o.exponent = 8 + end + + local val + if o.base == 2 then + val = size / math.pow(2, o.exponent * 10) + else + val = size / math.pow(1000, o.exponent) + end + + if o.bits then + val = val * 8 + if val > ceil then + val = val / ceil + o.exponent = o.exponent + 1 + end + end + + result = { + roundNumber(val, o.exponent > 0 and o.round or 0), + (o.base == 10 and o.exponent == 1) and (o.bits and "kb" or "kB") + or si[o.bits and "bits" or "bytes"][o.exponent + 1], + } + + if o.unix then + result[2] = result[2]:sub(1, 1) + + if result[2] == "b" or result[2] == "B" then + result = { + math.floor(result[1]), + "", + } + end + end + end + + assert(result) + + -- Decorating a 'diff' + if negative then + result[1] = -result[1] + end + + -- Applying custom suffix + result[2] = o.suffixes[result[2]] or result[2] + + -- Applying custom suffix + result[2] = o.suffixes[result[2]] or result[2] + + -- Returning Array, Object, or String (default) + if o.output == "array" then + return result + elseif o.output == "exponent" then + return o.exponent + elseif o.output == "object" then + return { + value = result[1], + suffix = result[2], + } + elseif o.output == "string" then + local value = tostring(result[1]) + value = value:gsub("%.0$", "") + local suffix = result[2] + return value .. o.spacer .. suffix + end +end + +return filesize diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/utils/init.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/utils/init.lua new file mode 100644 index 0000000..805ea7d --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/lua/neo-tree/utils/init.lua @@ -0,0 +1,1497 @@ +local uv = vim.uv or vim.loop +local log = require("neo-tree.log") +local compat = require("neo-tree.utils._compat") +local bit = require("bit") +local ffi_available, ffi = pcall(require, "ffi") + +local FILE_ATTRIBUTE_HIDDEN = 0x2 + +if ffi_available then + ffi.cdef([[ + int GetFileAttributesA(const char *path); + ]]) +end + +local M = {} + +local diag_severity_to_string = function(severity) + if severity == vim.diagnostic.severity.ERROR then + return "Error" + elseif severity == vim.diagnostic.severity.WARN then + return "Warn" + elseif severity == vim.diagnostic.severity.INFO then + return "Info" + elseif severity == vim.diagnostic.severity.HINT then + return "Hint" + else + return nil + end +end + +-- Backwards compatibility +M.pack = table.pack or function(...) + return { n = select("#", ...), ... } +end + +local tracked_functions = {} +---@enum neotree.utils.DebounceStrategy +M.debounce_strategy = { + CALL_FIRST_AND_LAST = 0, + CALL_LAST_ONLY = 1, +} + +---@enum neotree.utils.DebounceAction? +M.debounce_action = { + START_NORMAL = 0, + START_ASYNC_JOB = 1, + COMPLETE_ASYNC_JOB = 2, +} + +---Part of debounce. Moved out of the function to eliminate memory leaks. +---@param id string Identifier for the debounce group, such as the function name. +---@param frequency_in_ms number Miniumum amount of time between invocations of fn. +---@param strategy neotree.utils.DebounceStrategy The debounce_strategy to use, determines which calls to fn are not dropped. +---@param action neotree.utils.DebounceAction?? The debounce_action to use, determines how the function is invoked +local function defer_function(id, frequency_in_ms, strategy, action) + tracked_functions[id].in_debounce_period = true + vim.defer_fn(function() + local current_data = tracked_functions[id] + if not current_data then + return + end + if current_data.async_in_progress then + defer_function(id, frequency_in_ms, strategy, action) + return + end + local _fn = current_data.fn + current_data.fn = nil + current_data.in_debounce_period = false + if _fn ~= nil then + M.debounce(id, _fn, frequency_in_ms, strategy, action) + end + end, frequency_in_ms) +end + +---Call fn, but not more than once every x milliseconds. +---@param id string Identifier for the debounce group, such as the function name. +---@param fn function Function to be executed. +---@param frequency_in_ms number Miniumum amount of time between invocations of fn. +---@param strategy neotree.utils.DebounceStrategy The debounce_strategy to use, determines which calls to fn are not dropped. +---@param action neotree.utils.DebounceAction? The debounce_action to use, determines how the function is invoked +M.debounce = function(id, fn, frequency_in_ms, strategy, action) + local fn_data = tracked_functions[id] + + if fn_data == nil then + if action == M.debounce_action.COMPLETE_ASYNC_JOB then + -- original call complete and no further requests have been made + return + end + -- first call for this id + fn_data = { + id = id, + in_debounce_period = false, + fn = fn, + frequency_in_ms = frequency_in_ms, + } + tracked_functions[id] = fn_data + if strategy == M.debounce_strategy.CALL_LAST_ONLY then + defer_function(id, frequency_in_ms, strategy, action) + return + end + else + fn_data.fn = fn + fn_data.frequency_in_ms = frequency_in_ms + if action == M.debounce_action.COMPLETE_ASYNC_JOB then + fn_data.async_in_progress = false + return + elseif fn_data.async_in_progress then + defer_function(id, frequency_in_ms, strategy, action) + return + end + end + + if fn_data.in_debounce_period then + -- This id was called recently and can't be executed again yet. + -- Last one in wins. + return + end + + -- Run the requested function normally. + -- Use a pcall to ensure the debounce period is still respected even if + -- this call throws an error. + local success, result = true, nil + fn_data.in_debounce_period = true + if type(fn) == "function" then + success, result = pcall(fn) + end + ---not sure if this line is needed + ---@diagnostic disable-next-line: cast-local-type + fn = nil + fn_data.fn = fn + + if not success then + log.error("debounce ", id, " error: ", result) + elseif result and action == M.debounce_action.START_ASYNC_JOB then + -- This can't fire again until the COMPLETE_ASYNC_JOB signal is sent. + fn_data.async_in_progress = true + end + + if strategy == M.debounce_strategy.CALL_LAST_ONLY then + if fn_data.async_in_progress then + defer_function(id, frequency_in_ms, strategy, action) + else + -- We are done with this debounce + tracked_functions[id] = nil + end + else + -- Now schedule the next earliest execution. + -- If there are no calls to run the same function between now + -- and when this deferred executes, nothing will happen. + -- If there are several calls, only the last one in will run. + strategy = M.debounce_strategy.CALL_LAST_ONLY + defer_function(id, frequency_in_ms, strategy, action) + end +end + +--- Returns true if the contents of two tables are equal. +M.tbl_equals = function(table1, table2) + -- same object + if table1 == table2 then + return true + end + + -- not the same type + if type(table1) ~= "table" or type(table2) ~= "table" then + return false + end + + -- If tables are lists, check if they have the same values in the same order + if #table1 ~= #table2 then + return false + end + for i, v in ipairs(table1) do + if table2[i] ~= v then + return false + end + end + + -- Check if the tables have the same key/value pairs + for k, v in pairs(table1) do + if table2[k] ~= v then + return false + end + end + for k, v in pairs(table2) do + if table1[k] ~= v then + return false + end + end + + -- No differences found, tables are equal + return true +end + +M.execute_command = function(cmd) + local result = vim.fn.systemlist(cmd) + + -- An empty result is ok + if vim.v.shell_error ~= 0 or (#result > 0 and vim.startswith(result[1], "fatal:")) then + return false, {} + else + return true, result + end +end + +M.find_buffer_by_name = function(name) + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + local buf_name = vim.api.nvim_buf_get_name(buf) + if buf_name == name then + return buf + end + end + return -1 +end + +---Converts a filesize from libuv.stats into a human readable string with appropriate units. +---@param size any +---@return string +M.human_size = function(size) + local human = require("neo-tree.utils.filesize.filesize")(size, { output = "string" }) + ---@cast human string + return human +end + +---Converts a Unix timestamp into a human readable relative timestamps +---@param seconds integer +---@return string +M.relative_date = function(seconds) + local now = os.time() + local diff = now - seconds + + local function format(value, unit) + return value .. " " .. unit .. (value == 1 and "" or "s") .. " ago" + end + + if diff < 60 then + return "Just now" + elseif diff < 3600 then + local minutes = math.floor(diff / 60) + return format(minutes, "minute") + elseif diff < 86400 then + local hours = math.floor(diff / 3600) + return format(hours, "hour") + elseif diff < 86400 * 30 then + local days = math.floor(diff / 86400) + return format(days, "day") + elseif diff < 86400 * 365 then + local months = math.floor(diff / (86400 * 30)) + return format(months, "month") + end + local years = math.floor(diff / (86400 * 365)) + return format(years, "year") +end + +---@alias neotree.DateFormat string|"relative"|fun(integer):string + +---Formats dates. Supports relative dates as a preset, as well as custom formatting using arbitrary functions. +---Used to let users customize date formatting. +--- +---If `format` == "relative", it will use utils.relative_date to format. +---If `format` is a function, it should return a string for neo-tree to display. +---Else, `format` is presumed to be a format string for os.date(). +--- +---@see os.date() +---@param format neotree.DateFormat How to format `seconds` into a date string. +---@param seconds integer? Seconds since the platform epoch (Unix or otherwise). If nil, will be the current time. +---@return string formatted_date A string that represents the date. +M.date = function(format, seconds) + if not seconds then + seconds = os.time() + end + if format == "relative" then + return M.relative_date(seconds) + end + if type(format) == "function" then + return format(seconds) + end + local formatted_date = os.date(format, seconds) + if type(formatted_date) ~= "string" then + error('[neo-tree]: the format should not make os.date return a table (e.g. not "*t")') + end + return formatted_date +end + +---@class (exact) neotree.utils.DiagnosticCounts +---@field severity_number integer +---@field severity_string string +---@field Error integer? +---@field Warn integer? +---@field Info integer? +---@field Hint integer? + +---@alias neotree.utils.DiagnosticLookup table + +---Gets non-zero diagnostics counts for each open file and each ancestor directory. +---severity_number and severity_string refer to the highest severity with +---non-zero diagnostics count. +---Entry is nil if all counts are 0 +---@return neotree.utils.DiagnosticLookup +M.get_diagnostic_counts = function() + local lookup = {} + + for ns, _ in pairs(vim.diagnostic.get_namespaces()) do + for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do + local success, file_name = pcall(vim.api.nvim_buf_get_name, bufnr) + if success then + -- TODO: remove is_disabled check when dropping support for 0.8 + local enabled + if vim.diagnostic.is_enabled then + enabled = vim.diagnostic.is_enabled({ bufnr = bufnr, ns_id = ns }) + ---@diagnostic disable-next-line: deprecated + elseif vim.diagnostic.is_disabled then + ---@diagnostic disable-next-line: deprecated + enabled = not vim.diagnostic.is_disabled(bufnr, ns) + else + enabled = true + end + + if enabled then + for severity, _ in ipairs(vim.diagnostic.severity) do + local diagnostics = vim.diagnostic.get(bufnr, { namespace = ns, severity = severity }) + + if #diagnostics > 0 then + local severity_string = diag_severity_to_string(severity) + -- Get or create the entry for this file + local entry = lookup[file_name] + if entry == nil then + entry = { + severity_number = severity, + severity_string = severity_string, + } + lookup[file_name] = entry + end + -- Set the count for this diagnostic type + if severity_string ~= nil then + entry[severity_string] = #diagnostics + end + + -- Set the overall severity to the most severe so far + -- Error = 1, Warn = 2, Info = 3, Hint = 4 + if severity < entry.severity_number then + entry.severity_number = severity + entry.severity_string = severity_string + end + end + end + end + end + end + end + + for file_name, file_entry in pairs(lookup) do + -- Now bubble this status up to the parent directories + local parts = M.split(file_name, M.path_separator) + table.remove(parts) -- pop the last part so we don't override the file's status + M.reduce(parts, "", function(acc, part) + local path = (M.is_windows and acc == "") and part or M.path_join(acc, part) + + if file_entry.severity_number then + if not lookup[path] then + lookup[path] = { + severity_number = file_entry.severity_number, + severity_string = file_entry.severity_string, + } + else -- lookup[path].severity_number ~= nil + local min_severity = math.min(lookup[path].severity_number, file_entry.severity_number) + lookup[path].severity_number = min_severity + lookup[path].severity_string = diag_severity_to_string(min_severity) + end + end + + return path + end) + end + return lookup +end + +---@deprecated +---This will be removed in v4. Use `get_opened_buffers` instead. +---Gets a lookup of all open buffers keyed by path with the modifed flag as the value +---@return table opened_buffers +M.get_modified_buffers = function() + local opened_buffers = M.get_opened_buffers() + local copy = {} + for bufname, bufinfo in pairs(opened_buffers) do + copy[bufname] = bufinfo.modified + end + return copy +end + +---@class neotree.utils.OpenedBuffers +---@field modified boolean +---@field loaded boolean + +---Gets a lookup of all open buffers keyed by path with additional information +---@return table opened_buffers +M.get_opened_buffers = function() + local opened_buffers = {} + for _, buffer in ipairs(vim.api.nvim_list_bufs()) do + if vim.fn.buflisted(buffer) ~= 0 then + local buffer_name = vim.api.nvim_buf_get_name(buffer) + if buffer_name == nil or buffer_name == "" then + buffer_name = "[No Name]#" .. buffer + end + opened_buffers[buffer_name] = { + modified = vim.bo[buffer].modified, + loaded = vim.api.nvim_buf_is_loaded(buffer), + } + end + end + return opened_buffers +end + +---Resolves some variable to a string. The object can be either a string or a +--function that returns a string. +---@param functionOrString fun(node: NuiTree.Node, state: neotree.State):string The object to resolve. +---@param node NuiTree.Node The current node, which is passed to the function if it is a function. +---@param state neotree.State The current state, which is passed to the function if it is a function. +---@overload fun(functionOrString: string):string +---@return string string The resolved string. +M.getStringValue = function(functionOrString, node, state) + if type(functionOrString) == "function" then + return functionOrString(node, state) + else + return functionOrString + end +end + +---Return the keys of a given table. +---@param tbl string[] The table to get the keys of. +---@param sorted boolean Whether to sort the keys. +---@return string[] keys The keys of the table. +M.get_keys = function(tbl, sorted) + local keys = {} + for k, _ in pairs(tbl) do + table.insert(keys, k) + end + if sorted then + table.sort(keys) + end + return keys +end + +---Gets the usable columns in a window, subtracting sign, fold, and line number columns. +---@param winid integer The window id to get the columns of. +---@return number +M.get_inner_win_width = function(winid) + local info = vim.fn.getwininfo(winid) + if info and info[1] then + return info[1].width - info[1].textoff + end + log.error("Could not get window info for window", winid) + return vim.o.columns +end + +---@type table +local stat_providers = { + default = function(node) + return uv.fs_stat(node.path) + end, +} + +---@class neotree.utils.StatTime +--- @field sec number +---@class neotree.utils.StatTable +--- @field birthtime neotree.utils.StatTime +--- @field mtime neotree.utils.StatTime +--- @field size number + +--- Gets the statics for a node in the file system. The `stat` object will be cached +--- for the lifetime of the node. +---@param node table The Nui TreeNode node to get the stats for. +---@return neotree.utils.StatTable | table +M.get_stat = function(node) + if node.stat == nil then + local provider = stat_providers[node.stat_provider or "default"] + local success, stat = pcall(provider, node) + node.stat = success and stat or {} + end + return node.stat +end + +---Register a function to provide stats for a node. +---@param name string The name of the stat provider. +---@param func function The function to call to get the stats. +M.register_stat_provider = function(name, func) + stat_providers[name] = func + log.debug("Registered stat provider", name) +end + +---Handles null coalescing into a table at any depth. +---Use vim.tbl_get instead. +---@param sourceObject table The table to get a vlue from. +---@param valuePath string The path to the value to get. +---@param defaultValue any? The default value to return if the value is nil. +---@param strict_type_check boolean? Whether to require the type of the value is the same as the default value. +---@return any value The value at the path or the default value. +M.get_value = function(sourceObject, valuePath, defaultValue, strict_type_check) + if sourceObject == nil then + return defaultValue + end + local pathParts = M.split(valuePath, ".") + local currentTable = sourceObject + for _, part in ipairs(pathParts) do + if currentTable[part] == nil then + return defaultValue + else + currentTable = currentTable[part] + end + end + + if currentTable ~= nil then + return currentTable + end + if strict_type_check then + if type(defaultValue) == type(currentTable) then + return currentTable + else + return defaultValue + end + end +end + +---Sets a value at a path in a table, creating any missing tables along the way. +---@param sourceObject table The table to set a value in. +---@param valuePath string The path to the value to set. +---@param value any The value to set. +M.set_value = function(sourceObject, valuePath, value) + local pathParts = M.split(valuePath, ".") + local currentTable = sourceObject + for i, part in ipairs(pathParts) do + if i == #pathParts then + currentTable[part] = value + else + if type(currentTable[part]) ~= "table" then + currentTable[part] = {} + end + currentTable = currentTable[part] + end + end +end + +---Groups an array of items by a key. +---@param array table The array to group. +---@param key string The key to group by. +---@return table table The grouped array where the keys are the unique values of the specified key. +M.group_by = function(array, key) + local result = {} + for _, item in ipairs(array) do + local keyValue = item[key] + local group = result[keyValue] + if group == nil then + group = {} + result[keyValue] = group + end + table.insert(group, item) + end + return result +end + +---Determines if a file should be filtered by a given list of glob patterns. +---@param pattern_list table The list of glob patterns to filter by. +---@param path string The full path to the file. +---@param name string|nil The name of the file. +---@return boolean +M.is_filtered_by_pattern = function(pattern_list, path, name) + if pattern_list == nil then + return false + end + if name == nil then + _, name = M.split_path(path) + end + for _, p in ipairs(pattern_list) do + local separator_pattern = M.is_windows and "\\" or "/" + local filename = string.find(p, separator_pattern) and path or name + if string.find(filename or "", p) then + return true + end + end + return false +end + +---@param win_id integer? +M.is_floating = function(win_id) + win_id = win_id or vim.api.nvim_get_current_win() + local cfg = vim.api.nvim_win_get_config(win_id) + if cfg.relative > "" or cfg.external then + return true + end + return false +end + +M.is_winfixbuf = function(win_id) + if vim.fn.exists("&winfixbuf") == 1 then + win_id = win_id or vim.api.nvim_get_current_win() + return vim.wo[win_id].winfixbuf + end + return false +end + +---Evaluates the value of , which comes from an autocmd event, and determines if it +---is a valid file or some sort of utility buffer like quickfix or neo-tree itself. +---@param afile string The path or relative path to the file. +---@param true_for_terminals boolean? Whether to return true for terminals, normally it would be false. +---@return boolean boolean Whether the buffer is a real file. +M.is_real_file = function(afile, true_for_terminals) + if type(afile) ~= "string" or afile == "" or afile == "quickfix" then + return false + end + + local source = afile:match("^neo%-tree ([%l%-]+) %[%d+%]") + if source then + return false + end + + local success, bufnr = pcall(vim.fn.bufnr, afile) + if success and bufnr > 0 then + local buftype = vim.bo[bufnr].buftype + + if true_for_terminals and buftype == "terminal" then + return true + end + -- all other buftypes are not real files + if M.truthy(buftype) then + return false + end + return true + else + return false + end +end + +---Creates a new table from an array with the array items as keys. If a dict like +---table is passed in, those keys will be copied to a new table. +---@param tbl table The table to copy items from. +---@return table table A new dictionary style table. +M.list_to_dict = function(tbl) + local dict = {} + -- leave the existing keys + for key, val in pairs(tbl) do + dict[key] = val + end + -- and convert the number indexed items + for _, item in ipairs(tbl) do + dict[item] = true + end + return dict +end + +M.map = function(tbl, fn) + local t = {} + for k, v in pairs(tbl) do + t[k] = fn(v) + end + return t +end + +---Finds an appropriate window to open a file from neo-tree +---@param state neotree.State +---@param ignore_winfixbuf boolean? +M.get_appropriate_window = function(state, ignore_winfixbuf) + -- Avoid triggering autocommands when switching windows + local eventignore = vim.o.eventignore + vim.o.eventignore = "all" + + local current_window = vim.api.nvim_get_current_win() + + -- use last window if possible + local suitable_window_found = false + local nt = require("neo-tree") + local ignore_list = nt.config.open_files_do_not_replace_types or {} + local ignore = M.list_to_dict(ignore_list) + ignore["neo-tree"] = true + if nt.config.open_files_in_last_window then + local prior_window = nt.get_prior_window(ignore, ignore_winfixbuf) + if prior_window > 0 then + local success = pcall(vim.api.nvim_set_current_win, prior_window) + if success then + suitable_window_found = true + end + end + end + -- find a suitable window to open the file in + if not suitable_window_found then + if state.current_position == "right" then + vim.cmd("wincmd t") + else + vim.cmd("wincmd w") + end + end + local attempts = 0 + while attempts < 5 and not suitable_window_found do + local bt = vim.bo.buftype or "normal" + if ignore[vim.bo.filetype] or ignore[bt] or M.is_floating() then + attempts = attempts + 1 + vim.cmd("wincmd w") + elseif ignore_winfixbuf and M.is_winfixbuf() then + attempts = attempts + 1 + vim.cmd("wincmd w") + else + suitable_window_found = true + end + end + if not suitable_window_found then + -- go back to the neotree window, this will forve it to open a new split + vim.api.nvim_set_current_win(current_window) + end + + local winid = vim.api.nvim_get_current_win() + local is_neo_tree_window = vim.bo.filetype == "neo-tree" + vim.api.nvim_set_current_win(current_window) + + vim.o.eventignore = eventignore + + return winid, is_neo_tree_window +end + +---Resolves the width to a number +---@param width number|string|function +M.resolve_width = function(width) + local default_width = 40 + local available_width = vim.o.columns + if type(width) == "string" then + if string.sub(width, -1) == "%" then + width = tonumber(string.sub(width, 1, #width - 1)) / 100 + width = width * available_width + else + width = tonumber(width) or default_width + end + elseif type(width) == "function" then + width = width() + end + + if type(width) ~= "number" then + width = default_width + end + + return math.floor(width) +end + +M.force_new_split = function(current_position, escaped_path) + local result, err + local split_command = "vsplit" + -- respect window position in user config when Neo-tree is the only window + if current_position == "left" then + split_command = "rightbelow vs" + elseif current_position == "right" then + split_command = "leftabove vs" + end + if escaped_path == M.escape_path_for_cmd("[No Name]") then + -- vim's default behavior is to overwrite [No Name] buffers. + -- We need to split first and then open the path to workaround this behavior. + ---@diagnostic disable-next-line: param-type-mismatch + result, err = pcall(vim.cmd, split_command) + if result then + vim.cmd.edit(escaped_path) + end + else + ---@diagnostic disable-next-line: param-type-mismatch + result, err = pcall(vim.cmd, split_command .. " " .. escaped_path) + end + return result, err +end + +---Open file in the appropriate window. +---@param state neotree.State +---@param path string The file to open +---@param open_cmd string? The vimcommand to use to open the file +---@param bufnr number|nil The buffer number to open +M.open_file = function(state, path, open_cmd, bufnr) + open_cmd = open_cmd or "edit" + -- If the file is already open, switch to it. + bufnr = bufnr or M.find_buffer_by_name(path) + if bufnr <= 0 then + bufnr = nil + else + local buf_cmd_lookup = + { edit = "b", e = "b", split = "sb", sp = "sb", vsplit = "vert sb", vs = "vert sb" } + local cmd_for_buf = buf_cmd_lookup[open_cmd] + if cmd_for_buf then + open_cmd = cmd_for_buf + else + bufnr = nil + end + end + + if M.truthy(path) then + local relative = require("neo-tree").config.open_files_using_relative_paths + local escaped_path = M.escape_path_for_cmd(relative and vim.fn.fnamemodify(path, ":.") or path) + local bufnr_or_path = bufnr or escaped_path + local events = require("neo-tree.events") + local result = true + local err = nil + local event_result = events.fire_event(events.FILE_OPEN_REQUESTED, { + state = state, + path = path, + open_cmd = open_cmd, + bufnr = bufnr, + }) or {} + if event_result.handled then + events.fire_event(events.FILE_OPENED, path) + return + end + if state.current_position == "current" then + ---@diagnostic disable-next-line: param-type-mismatch + result, err = pcall(vim.cmd, open_cmd .. " " .. bufnr_or_path) + else + local winid, is_neo_tree_window = M.get_appropriate_window(state) + vim.api.nvim_set_current_win(winid) + -- TODO: make this configurable, see issue #43 + if is_neo_tree_window then + local width = vim.api.nvim_win_get_width(0) + if width == vim.o.columns then + -- Neo-tree must be the only window, restore it's status as a sidebar + width = M.get_value(state, "window.width", 40, false) + width = M.resolve_width(width) + end + result, err = M.force_new_split(state.current_position, escaped_path) + vim.api.nvim_win_set_width(winid, width) + else + ---@diagnostic disable-next-line: param-type-mismatch + result, err = pcall(vim.cmd, open_cmd .. " " .. bufnr_or_path) + end + end + if not result and string.find(err or "", "winfixbuf") and M.is_winfixbuf() then + local winid, is_neo_tree_window = M.get_appropriate_window(state, true) + -- Rescan window list to find a window that is not winfixbuf. + -- If found, retry executing command in that window, + -- otherwise, all windows are either neo-tree or winfixbuf so we make a new split. + if not is_neo_tree_window and not M.is_winfixbuf(winid) then + vim.api.nvim_set_current_win(winid) + ---@diagnostic disable-next-line: param-type-mismatch + result, err = pcall(vim.cmd, open_cmd .. " " .. bufnr_or_path) + else + result, err = M.force_new_split(state.current_position, escaped_path) + end + end + if result or err == "Vim(edit):E325: ATTENTION" then + -- fixes #321 + vim.bo[0].buflisted = true + events.fire_event(events.FILE_OPENED, path) + else + log.error("Error opening file:", err) + end + end +end + +M.reduce = function(list, memo, func) + for _, i in ipairs(list) do + memo = func(memo, i) + end + return memo +end + +M.reverse_list = function(list) + local result = {} + for i = #list, 1, -1 do + table.insert(result, list[i]) + end + return result +end + +---@param state neotree.State|neotree.Config.Base +---@param config_option string +---@param default_value any +M.resolve_config_option = function(state, config_option, default_value) + local opt = M.get_value(state, config_option, default_value, false) + if type(opt) == "function" then + local success, val = pcall(opt, state) + if success then + return val + else + log.error("Error resolving config option: " .. config_option .. ": " .. val) + return default_value + end + else + return opt + end +end + +---Normalize a path, to avoid errors when comparing paths. +---@param path string The path to be normalize. +---@return string string The normalized path. +M.normalize_path = function(path) + if M.is_windows then + -- normalize the drive letter to uppercase + path = path:sub(1, 1):upper() .. path:sub(2) + -- Turn mixed forward and back slashes into all forward slashes + -- using NeoVim's logic + path = vim.fs.normalize(path, { win = true }) + -- Now use backslashes, as expected by the rest of Neo-Tree's code + path = path:gsub("/", M.path_separator) + end + return path +end + +---Check if a path is a subpath of another. +---@param base string The base path. +---@param path string The path to check is a subpath. +---@return boolean boolean True if it is a subpath, false otherwise. +M.is_subpath = function(base, path) + if not M.truthy(base) or not M.truthy(path) then + return false + elseif base == path then + return true + end + + base = M.normalize_path(base) + path = M.normalize_path(path) + if path:sub(1, #base) == base then + local base_parts = M.split(base, M.path_separator) + local path_parts = M.split(path, M.path_separator) + for i, part in ipairs(base_parts) do + if path_parts[i] ~= part then + return false + end + end + return true + end + return false +end + +---The file system path separator for the current platform. +M.path_separator = "/" +M.is_windows = vim.fn.has("win32") == 1 or vim.fn.has("win32unix") == 1 +if M.is_windows == true then + M.path_separator = "\\" +end + +M.is_macos = vim.fn.has("mac") == 1 + +---Remove the path separator from the end of a path in a cross-platform way. +---@param path string The path to remove the separator from. +---@return string string The path without any trailing separator. +---@return number count The number of separators removed. +M.remove_trailing_slash = function(path) + if M.is_windows then + return path:gsub("\\$", "") + else + return path:gsub("/$", "") + end +end + +---Sorts a list of paths in the order they would appear in a tree. +---@param paths table The list of paths to sort. +---@return table table The sorted list of paths. +M.sort_by_tree_display = function(paths) + -- first turn the paths into a true tree + local nodes = {} + local index = {} + local function create_nodes(path) + local node = index[path] + if node then + return node + end + local parent, name = M.split_path(path) + node = { + name = name, + path = path, + children = {}, + } + index[path] = node + if parent == nil then + table.insert(nodes, node) + else + local parent_node = index[parent] + if parent_node == nil then + parent_node = create_nodes(parent) + end + table.insert(parent_node.children, node) + end + return node + end + + for _, path in ipairs(paths) do + create_nodes(path) + end + + -- create a lookup of the original paths so that we don't return anything + -- that isn't in the original list + local original_paths = M.list_to_dict(paths) + + -- sort folders before files + local sort_by_name = function(a, b) + local a_isdir = #a.children > 0 + local b_isdir = #b.children > 0 + if a_isdir and not b_isdir then + return true + elseif not a_isdir and b_isdir then + return false + else + return a.name < b.name + end + end + + -- now we can walk the tree in the order that it would be displayed on the screen + local result = {} + local function walk_tree(node) + if original_paths[node.path] then + table.insert(result, node.path) + original_paths[node.path] = nil -- just to be sure we don't return it twice + end + table.sort(node.children, sort_by_name) + for _, child in ipairs(node.children) do + walk_tree(child) + end + end + + walk_tree({ children = nodes }) + return result +end + +---Split string into a table of strings using a separator. +---@param inputString string The string to split. +---@param sep string The separator to use. +---@return table table A table of strings. +M.split = function(inputString, sep) + local fields = {} + + local pattern = string.format("([^%s]+)", sep) + local _ = string.gsub(inputString, pattern, function(c) + fields[#fields + 1] = c + end) + + return fields +end + +---Split a path into a parentPath and a name. +---@param path string? The path to split. +---@return string? parentPath +---@return string? name +M.split_path = function(path) + if not path then + return nil, nil + end + if path == M.path_separator then + return nil, M.path_separator + end + local parts = M.split(path, M.path_separator) + local name = table.remove(parts) + local parentPath = table.concat(parts, M.path_separator) + if M.is_windows then + if #parts == 1 then + parentPath = parentPath .. M.path_separator + elseif parentPath == "" then + return nil, name + end + else + parentPath = M.path_separator .. parentPath + end + return parentPath, name +end + +---Joins arbitrary number of paths together. +---@param ... string The paths to join. +---@return string +M.path_join = function(...) + local args = { ... } + if #args == 0 then + return "" + end + + local all_parts = {} + if type(args[1]) == "string" and args[1]:sub(1, 1) == M.path_separator then + all_parts[1] = "" + end + + for _, arg in ipairs(args) do + if arg == "" and #all_parts == 0 and not M.is_windows then + all_parts = { "" } + else + local arg_parts = M.split(arg, M.path_separator) + vim.list_extend(all_parts, arg_parts) + end + end + return table.concat(all_parts, M.path_separator) +end + +local table_merge_internal +---Merges overrideTable into baseTable. This mutates baseTable. +---@param base_table table The base table that provides default values. +---@param override_table table The table to override the base table with. +---@return table table The merged table. +table_merge_internal = function(base_table, override_table) + for k, v in pairs(override_table) do + if type(v) == "table" then + if type(base_table[k]) == "table" then + table_merge_internal(base_table[k], v) + else + base_table[k] = v + end + else + base_table[k] = v + end + end + return base_table +end + +---@deprecated +---Use +---```lua +---vim.deepcopy(source_table, true) +---``` +---instead. +M.table_copy = function(source_table) + return vim.deepcopy(source_table, compat.noref()) +end + +---@deprecated +---Use: +---```lua +---vim.tbl_deep_extend("force", base_table, source_table) instead. +---``` +---instead. +M.table_merge = function(base_table, override_table) + local merged_table = table_merge_internal({}, base_table) + return table_merge_internal(merged_table, override_table) +end + +---Evaluate the truthiness of a value, according to js/python rules. +---@param value any +---@return boolean truthy +M.truthy = function(value) + if value == nil then + return false + end + if type(value) == "boolean" then + return value + end + if type(value) == "string" then + return value > "" + end + if type(value) == "number" then + return value > 0 + end + if type(value) == "table" then + return next(value) ~= nil + end + return true +end + +M.is_expandable = function(node) + return node:has_children() or node.type == "directory" +end + +M.windowize_path = function(path) + return path:gsub("/", "\\") +end + +---Escapes a path primarily relying on `vim.fn.fnameescape`. This function should +---only be used when preparing a path to be used in a vim command, such as `:e`. +--- +---For Windows systems, this function handles punctuation characters that will +---be escaped, but may appear at the beginning of a path segment. For example, +---the path `C:\foo\(bar)\baz.txt` (where foo, (bar), and baz.txt are segments) +---will remain unchanged when escaped by `fnaemescape` on a Windows system. +---However, if that string is used to edit a file with `:e`, `:b`, etc., the open +---parenthesis will be treated as an escaped character and the path separator will +---be lost. +--- +---For more details, see issue #889 when this function was introduced, and further +---discussions in #1264, #1352, and #1448. +---@param path string +---@return string +M.escape_path_for_cmd = function(path) + local escaped_path = vim.fn.fnameescape(path) + if M.is_windows then + -- there is too much history to this logic to capture in a reasonable comment. + -- essentially, the following logic adds a number of `\` depending on the leading + -- character in a path segment. see #1264, #1352, and #1448 for more info. + local need_extra_esc = path:find("[%[%]`%$~]") + local esc = need_extra_esc and "\\\\" or "\\" + escaped_path = escaped_path:gsub("\\[%(%)%^&;]", esc .. "%1") + if need_extra_esc then + escaped_path = escaped_path:gsub("\\\\['` ]", "\\%1") + end + end + return escaped_path +end + +M.wrap = function(func, ...) + if type(func) ~= "function" then + error("Expected function, got " .. type(func)) + end + local wrapped_args = { ... } + return function(...) + local all_args = M.pack(unpack(wrapped_args), ...) + func(unpack(all_args)) + end +end + +---Checks if the given path is hidden using the Windows hidden file/directory logic +---@param path string +---@return boolean +function M.is_hidden(path) + if ffi_available and M.is_windows then + return bit.band(ffi.C.GetFileAttributesA(path), FILE_ATTRIBUTE_HIDDEN) ~= 0 + else + return false + end +end + +---Returns a new list that is the result of dedeuplicating a list. +---@param list table The list to deduplicate. +---@return table table The list of unique values. +M.unique = function(list) + local seen = {} + local result = {} + for _, item in ipairs(list) do + if not seen[item] then + table.insert(result, item) + seen[item] = true + end + end + return result +end + +---Splits string by sep on first occurrence. brace_expand_split("a,b,c", ",") -> { "a", "b,c" }. nil if separator not found. +---@param s string: input string +---@param separator string: separator +---@return string, string | nil +local brace_expand_split = function(s, separator) + local pos = 1 + local depth = 0 + while pos <= s:len() do + local c = s:sub(pos, pos) + if c == "\\" then + pos = pos + 1 + elseif c == separator and depth == 0 then + return s:sub(1, pos - 1), s:sub(pos + 1) + elseif c == "{" then + depth = depth + 1 + elseif c == "}" then + if depth > 0 then + depth = depth - 1 + end + end + pos = pos + 1 + end + return s, nil +end + +---@param tbl table +local function flatten(tbl) + if vim.iter then + return vim.iter(tbl):flatten():totable() + end + + ---@diagnostic disable-next-line: deprecated + return vim.tbl_flatten(tbl) +end +---Perform brace expansion on a string and return the sequence of the results +---@param s string?: input string which is inside braces, if nil return { "" } +---@return string[] | nil: list of strings each representing the individual expanded strings +local brace_expand_contents = function(s) + if s == nil then -- no closing brace "}" + return { "" } + elseif s == "" then -- brace with no content "{}" + return { "{}" } + end + + ---Generate a sequence from from..to..step and apply `func` + ---@param from string | number: initial value + ---@param to string | number: end value + ---@param step string | number: step value + ---@param func fun(i: number): string | nil function(string | number) -> string | nil: function applied to all values in sequence. if return is nil, the value will be ignored. + ---@return string[]: generated string list + ---@private + local function resolve_sequence(from, to, step, func) + local f, t = tonumber(from), tonumber(to) + local st = (t < f and -1 or 1) * math.abs(tonumber(step) or 1) -- reverse (negative) step if t < f + ---@type string[] + local items = {} + for i = f, t, st do + local r = func(i) + if r ~= nil then + table.insert(items, r) + end + end + return items + end + + ---@alias neotree.utils.Resolver fun(from: string, to: string, step: string): string[] + + ---If pattern matches the input string `s`, apply an expansion by `resolve_func` + ---@param pattern string: regex to match on `s` + ---@param resolve_func neotree.utils.Resolver + ---@return string[]|nil sequence Expanded sequence or nil if failed + local function try_sequence_on_pattern(pattern, resolve_func) + local from, to, step = string.match(s, pattern) + if from then + return resolve_func(from, to, step) + end + return nil + end + + ---Process numeric sequence expression. e.g. {0..2} -> {0,1,2}, {01..05..2} -> {01,03,05} + local resolve_sequence_num = function(from, to, step) + local format = "%d" + -- Pad strings in the presence of a leading zero + local pattern = "^-?0%d" + if from:match(pattern) or to:match(pattern) then + format = "%0" .. math.max(#from, #to) .. "d" + end + return resolve_sequence(from, to, step, function(i) + return string.format(format, i) + end) + end + + ---Process alphabet sequence expression. e.g. {a..c} -> {a,b,c}, {a..e..2} -> {a,c,e} + local resolve_sequence_char = function(from, to, step) + return resolve_sequence(from:byte(), to:byte(), step, function(i) + return i ~= 92 and string.char(i) or nil -- 92 == '\\' is ignored in bash + end) + end + + ---@type table + local check_list = { + { [=[^(-?%d+)%.%.(-?%d+)%.%.(-?%d+)$]=], resolve_sequence_num }, + { [=[^(-?%d+)%.%.(-?%d+)$]=], resolve_sequence_num }, + { [=[^(%a)%.%.(%a)%.%.(-?%d+)$]=], resolve_sequence_char }, + { [=[^(%a)%.%.(%a)$]=], resolve_sequence_char }, + } + for _, list in ipairs(check_list) do + local regex, func = list[1], list[2] + local sequence = try_sequence_on_pattern(regex, func) + if sequence then + return sequence + end + end + + -- Regular `,` separated expression. x{a,b,c} -> {xa,xb,xc} + local items, tmp_s = {}, nil + tmp_s = s + while tmp_s ~= nil do + items[#items + 1], tmp_s = brace_expand_split(tmp_s, ",") + end + if #items == 1 then -- Only one expansion found. Abort. + return nil + end + return flatten(items) +end + +---brace_expand: +-- Perform a BASH style brace expansion to generate arbitrary strings. +-- Especially useful for specifying structured file / dir names. +-- USAGE: +-- - `require("neo-tree.utils").brace_expand("x{a..e..2}")` -> `{ "xa", "xc", "xe" }` +-- - `require("neo-tree.utils").brace_expand("file.txt{,.bak}")` -> `{ "file.txt", "file.txt.bak" }` +-- - `require("neo-tree.utils").brace_expand("./{a,b}/{00..02}.lua")` -> `{ "./a/00.lua", "./a/01.lua", "./a/02.lua", "./b/00.lua", "./b/01.lua", "./b/02.lua" }` +-- More examples for BASH style brace expansion can be found here: https://facelessuser.github.io/bracex/ +---@param s string: input string. e.g. {a..e..2} -> {a,c,e}, {00..05..2} -> {00,03,05} +---@return string[]: result of expansion, array with at least one string (one means it failed to expand and the raw string is returned) +M.brace_expand = function(s) + local preamble, postamble = brace_expand_split(s, "{") + if postamble == nil then + return { s } + end + + local expr, postscript, contents = nil, nil, nil + postscript = postamble + while contents == nil do + local old_expr = expr + expr, postscript = brace_expand_split(postscript, "}") + if old_expr then + expr = old_expr .. "}" .. expr + end + if postscript == nil then -- No closing brace found, so we put back the unmatched '{' + preamble = preamble .. "{" + expr, postscript = nil, postamble + end + contents = brace_expand_contents(expr) + end + + -- Concat everything. Pass postscript recursively. + ---@type string[] + local result = {} + for _, item in ipairs(contents) do + for _, suffix in ipairs(M.brace_expand(postscript)) do + result[#result + 1] = table.concat({ preamble, item, suffix }) + end + end + return result +end + +---Indexes a table that uses paths as keys. Case-insensitive logic is used when +---running on Windows. +--- +---Consideration should be taken before using this function, because it is a +---bit expensive on Windows. However, this function helps when trying to index +---with absolute path keys, which can have inconsistent casing on Windows (such +---as with drive letters). +---@param tbl table +---@param key string +---@return unknown +M.index_by_path = function(tbl, key) + local value = tbl[key] + if value ~= nil then + return value + end + + -- on windows, paths that differ only by case are considered equal + -- TODO: we should optimize this, see discussion in #1353 + if M.is_windows then + local key_lower = key:lower() + for k, v in pairs(tbl) do + if key_lower == k:lower() then + return v + end + end + end + + return value +end + +---Backport of vim.keycode +---@see vim.keycode +---@param str string +---@return string representation Internal representation of the keycodes +function M.keycode(str) + return vim.api.nvim_replace_termcodes(str, true, true, true) +end + +---Iterate through a table, sorted by its keys. +---Compared to vim.spairs, it also accepts a method that specifies how to sort the table by key. +--- +---@see vim.spairs +---@see table.sort +--- +---@generic T: table, K, V +---@param t T Dict-like table +---@param sorter? fun(a: K, b: K):boolean A function that returns true if a is less than b. +---@return fun(table: table, index?: K):K, V # |for-in| iterator over sorted keys and their values +---@return T +function M.spairs(t, sorter) + -- collect the keys + local keys = {} + for k in pairs(t) do + table.insert(keys, k) + end + table.sort(keys, sorter) + + -- Return the iterator function. + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keys[i], t[keys[i]] + end + end, + t +end + +local strwidth = vim.api.nvim_strwidth +local strcharpart, strchars = vim.fn.strcharpart, vim.fn.strchars +local slice = vim.fn.exists("*slice") == 1 and vim.fn.slice + or function(str, start, _end) + local char_amount = strchars(str) + _end = _end or char_amount + _end = _end < 0 and (char_amount + _end) or _end + return strcharpart(str, start, _end) + end + +-- Function below provided by @akinsho, modified by @pynappo +-- https://github.com/nvim-neo-tree/neo-tree.nvim/pull/427#discussion_r924947766 +-- TODO: maybe use vim.stf_utf* functions instead of strchars, once neovim updates enough + +-- Truncate a string based on number of display columns/cells it occupies +-- so that multibyte characters are not broken up mid-character +---@param str string +---@param col_limit number +---@param align 'left'|'right'|nil +---@return string shortened +---@return number width +M.truncate_by_cell = function(str, col_limit, align) + local width = strwidth(str) + if width <= col_limit then + return str, width + end + local short = str + if align == "right" then + short = slice(short, 1) + while strwidth(short) > col_limit do + short = slice(short, 1) + end + else + short = slice(short, 0, -1) + while strwidth(short) > col_limit do + short = slice(short, 0, -1) + end + end + return short, strwidth(short) +end + +---@type table +M.prior_windows = {} + +return M diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/mise.toml b/.config/nvim/pack/tree/start/neo-tree.nvim/mise.toml new file mode 100644 index 0000000..4e8b2ba --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/mise.toml @@ -0,0 +1,5 @@ +[tools] +cargo-binstall = "latest" +"cargo:emmylua_check" = "latest" +"cargo:emmylua_ls" = "latest" +lua-language-server = "latest" diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/plugin/neo-tree.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/plugin/neo-tree.lua new file mode 100644 index 0000000..68b4317 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/plugin/neo-tree.lua @@ -0,0 +1,157 @@ +if vim.g.loaded_neo_tree == 1 or vim.g.loaded_neo_tree == true then + return +end + +-- Possibly convert this to lua using customlist instead of custom in the future? +vim.api.nvim_create_user_command("Neotree", function(ctx) + require("neo-tree.command")._command(unpack(ctx.fargs)) +end, { + nargs = "*", + complete = "custom,v:lua.require'neo-tree.command'.complete_args", +}) + +---@param path string? The path to check +---@return boolean hijacked Whether we hijacked a buffer +local function try_netrw_hijack(path) + if not path or #path == 0 then + return false + end + + local stats = (vim.uv or vim.loop).fs_stat(path) + if not stats or stats.type ~= "directory" then + return false + end + + return require("neo-tree.setup.netrw").hijack() +end + +local augroup = vim.api.nvim_create_augroup("NeoTree", { clear = true }) + +-- lazy load until bufenter/netrw hijack +vim.api.nvim_create_autocmd({ "BufEnter" }, { + group = augroup, + desc = "Lazy-load until bufenter/opened dir", + callback = function(args) + return vim.g.neotree_watching_bufenter == 1 or try_netrw_hijack(args.file) + end, +}) + +-- track window order +vim.api.nvim_create_autocmd({ "WinEnter" }, { + group = augroup, + desc = "Track prior windows for opening intuitiveness", + callback = function(ev) + local win = vim.api.nvim_get_current_win() + local utils = require("neo-tree.utils") + if utils.is_floating(win) then + return + end + + if vim.bo[ev.buf].filetype == "neo-tree" then + return + end + + local tabid = vim.api.nvim_get_current_tabpage() + utils.prior_windows[tabid] = utils.prior_windows[tabid] or {} + local tab_windows = utils.prior_windows[tabid] + table.insert(tab_windows, win) + + -- prune history + local win_count = #tab_windows + if win_count > 100 then + if table.move then + utils.prior_windows[tabid] = + require("neo-tree.utils._compat").table_move(tab_windows, 80, win_count, 1, {}) + return + end + + local new_array = {} + for i = 80, win_count do + table.insert(new_array, tab_windows[i]) + end + utils.prior_windows[tabid] = new_array + end + end, +}) + +-- setup session loading +vim.api.nvim_create_autocmd("SessionLoadPost", { + group = augroup, + desc = "Session loading", + callback = function() + if require("neo-tree").ensure_config().auto_clean_after_session_restore then + require("neo-tree.ui.renderer").clean_invalid_neotree_buffers(true) + end + end, +}) + +vim.api.nvim_create_autocmd("WinClosed", { + group = augroup, + desc = "close_if_last_window autocmd", + callback = function(args) + local closing_win = tonumber(args.match) + local visible_winids = vim.api.nvim_tabpage_list_wins(0) + local other_panes = {} + local utils = require("neo-tree.utils") + for _, winid in ipairs(visible_winids) do + if not utils.is_floating(winid) and winid ~= closing_win then + other_panes[#other_panes + 1] = winid + end + end + + if #other_panes ~= 1 then + return + end + + local remaining_pane = other_panes[1] + local remaining_buf = vim.api.nvim_win_get_buf(remaining_pane) + + if vim.bo[remaining_buf].filetype ~= "neo-tree" then + return + end + + local position = vim.b[remaining_buf].neo_tree_position + local source = vim.b[remaining_buf].neo_tree_source + -- close_if_last_window just doesn't make sense for a split style + if position == "current" then + return + end + + local log = require("neo-tree.log") + log.trace("last window, closing") + local state = require("neo-tree.sources.manager").get_state(source) + if not state then + return + end + if not require("neo-tree").ensure_config().close_if_last_window then + return + end + local mod = utils.get_opened_buffers() + log.debug("close_if_last_window, modified files found: ", vim.inspect(mod)) + for filename, buf_info in pairs(mod) do + if buf_info.modified then + local buf_name, message + if vim.startswith(filename, "[No Name]#") then + buf_name = string.sub(filename, 11) + message = + "Cannot close because an unnamed buffer is modified. Please save or discard this file." + else + buf_name = filename + message = + "Cannot close because one of the files is modified. Please save or discard changes." + end + log.trace("close_if_last_window, showing unnamed modified buffer: ", filename) + vim.schedule(function() + log.warn(message) + vim.cmd("rightbelow vertical split") + vim.api.nvim_win_set_width(0, state.window.width or 40) + vim.cmd("b " .. buf_name) + end) + return + end + end + vim.cmd("qa!") + end, +}) + +vim.g.loaded_neo_tree = 1 diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/release.sh b/.config/nvim/pack/tree/start/neo-tree.nvim/release.sh new file mode 100755 index 0000000..2053009 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/release.sh @@ -0,0 +1,38 @@ +#!/bin/bash +REPO="nvim-neo-tree/neo-tree.nvim" +LAST_VERSION=$(curl --silent "https://api.github.com/repos/$REPO/releases/latest" | jq -r .tag_name) +echo "LAST_VERSION=$LAST_VERSION" +MAJOR=$(cut -d. -f1 <<<"$LAST_VERSION") +MINOR=$(cut -d. -f2 <<<"$LAST_VERSION") +echo + +RELEASE_BRANCH="${1:-v${MAJOR}.x}" +echo "RELEASE_BRANCH=$RELEASE_BRANCH" +NEXT_VERSION=$MAJOR.$((MINOR+1)) +NEW_VERSION="${2:-${NEXT_VERSION}}" +echo "NEW_VERSION=$NEW_VERSION" +echo + +read -p "Are you sure you want to publish this release? " -n 1 -r +echo # (optional) move to a new line +if [[ ! $REPLY =~ ^[Yy]$ ]] +then + [[ "$0" = "$BASH_SOURCE" ]] && exit 1 || return 1 # handle exits from shell or function but don't exit interactive shell +fi + +git fetch +git checkout main +git pull +echo "Merging to ${RELEASE_BRANCH}" +git checkout $RELEASE_BRANCH +git pull +if git merge --ff-only origin/main; then + git push + git tag -a $NEW_VERSION -m "Release ${NEW_VERSION}" + git push origin $NEW_VERSION + echo "Creating Release" + gh release create $NEW_VERSION --generate-notes +else + echo "RELEASE FAILED! Could not fast-forward release to $RELEASE_BRANCH" +fi +git checkout main diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/mininit.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/mininit.lua new file mode 100644 index 0000000..43dfdca --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/mininit.lua @@ -0,0 +1,13 @@ +local root_dir = vim.fs.find("neo-tree.nvim", { upward = true, limit = 1 })[1] +assert(root_dir, "no neo-tree found") + +package.path = ("%s;%s/?.lua;%s/?/init.lua"):format(package.path, root_dir, root_dir) +vim.opt.packpath:prepend(root_dir .. "/.dependencies") + +vim.opt.rtp = { + root_dir, + vim.env.VIMRUNTIME, +} + +-- need this for tests to work +vim.cmd.source(root_dir .. "/plugin/neo-tree.lua") diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/command/command_current_spec.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/command/command_current_spec.lua new file mode 100644 index 0000000..e532988 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/command/command_current_spec.lua @@ -0,0 +1,107 @@ +pcall(require, "luacov") + +local Path = require("plenary.path") +local u = require("tests.utils") +local verify = require("tests.utils.verify") + +local run_in_current_command = function(command, expected_tree_node) + local winid = vim.api.nvim_get_current_win() + + vim.cmd(command) + verify.window_handle_is(winid) + verify.buf_name_endswith(string.format("neo-tree filesystem [%s]", winid), 1000) + if expected_tree_node then + verify.filesystem_tree_node_is(expected_tree_node, winid) + end +end + +local run_close_command = function(command) + vim.cmd(command) + u.wait_for(function() end, { interval = 200, timeout = 200 }) +end + +describe("Command", function() + local test = u.fs.init_test({ + items = { + { + name = "foo", + type = "dir", + items = { + { + name = "bar", + type = "dir", + items = { + { name = "baz1.txt", type = "file" }, + { name = "baz2.txt", type = "file", id = "deepfile2" }, + }, + }, + }, + }, + { name = "topfile1.txt", type = "file", id = "topfile1" }, + { name = "topfile2.txt", type = "file", id = "topfile2" }, + }, + }) + + test.setup() + + local fs_tree = test.fs_tree + + after_each(function() + u.clear_environment() + end) + + describe("netrw style:", function() + it("`:Neotree current` should show neo-tree in current window", function() + local cmd = "Neotree current" + run_in_current_command(cmd) + end) + + it( + "`:Neotree current reveal` should show neo-tree and reveal file in current window", + function() + local cmd = "Neotree current reveal" + local testfile = fs_tree.lookup["topfile1"].abspath + u.editfile(testfile) + run_in_current_command(cmd, testfile) + end + ) + + it("`:Neotree current reveal toggle` should toggle neo-tree in current window", function() + local cmd = "Neotree current reveal toggle" + local testfile = fs_tree.lookup["topfile1"].abspath + u.editfile(testfile) + local tree_winid = vim.api.nvim_get_current_win() + + -- toggle OPEN + run_in_current_command(cmd, testfile) + + -- toggle CLOSE + run_close_command(cmd) + verify.window_handle_is(tree_winid) + verify.buf_name_is(testfile) + end) + + it( + "`:Neotree current reveal_force_cwd reveal_file=xyz` should reveal file current window if cwd is not a parent of file", + function() + vim.cmd("cd ~") + local testfile = fs_tree.lookup["deepfile2"].abspath + local cmd = "Neotree current reveal_force_cwd reveal_file=" .. testfile + run_in_current_command(cmd, testfile) + end + ) + + it( + "`:Neotree current reveal_force_cwd reveal_file=xyz` should reveal file current window if cwd is a parent of file", + function() + local testfile = fs_tree.lookup["deepfile2"].abspath + local testfile_dir = Path:new(testfile):parent().filename + vim.cmd(string.format("cd %s", testfile_dir)) + local cmd = "Neotree current reveal_force_cwd reveal_file=" .. testfile + run_in_current_command(cmd, testfile) + end + ) + end) + + test.teardown() +end) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/command/command_spec.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/command/command_spec.lua new file mode 100644 index 0000000..8bc2355 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/command/command_spec.lua @@ -0,0 +1,214 @@ +pcall(require, "luacov") + +local u = require("tests.utils") +local verify = require("tests.utils.verify") + +local run_focus_command = function(command, expected_tree_node) + local winid = vim.api.nvim_get_current_win() + + vim.cmd(command) + u.wait_for_neo_tree({ interval = 10, timeout = 200 }) + --u.wait_for_neo_tree() + verify.window_handle_is_not(winid) + verify.buf_name_endswith("neo-tree filesystem [1]") + if expected_tree_node then + verify.filesystem_tree_node_is(expected_tree_node) + end +end + +local run_show_command = function(command, expected_tree_node) + local starting_winid = vim.api.nvim_get_current_win() + local starting_bufname = vim.api.nvim_buf_get_name(0) + local expected_num_windows = #vim.api.nvim_list_wins() + 1 + + vim.cmd(command) + verify.eventually(500, function() + if #vim.api.nvim_list_wins() ~= expected_num_windows then + return false + end + if vim.api.nvim_get_current_win() ~= starting_winid then + return false + end + if vim.api.nvim_buf_get_name(0) ~= starting_bufname then + return false + end + if expected_tree_node then + verify.filesystem_tree_node_is(expected_tree_node) + end + return true + end, "Expected to see a new window without focusing it.") +end + +local run_close_command = function(command) + vim.cmd(command) + u.wait_for(function() end, { interval = 200, timeout = 200 }) +end + +describe("Command", function() + local test = u.fs.init_test({ + items = { + { + name = "foo", + type = "dir", + items = { + { + name = "bar", + type = "dir", + items = { + { name = "baz1.txt", type = "file" }, + { name = "baz2.txt", type = "file", id = "deepfile2" }, + }, + }, + { name = "foofile1.txt", type = "file" }, + }, + }, + { name = "topfile1.txt", type = "file", id = "topfile1" }, + }, + }) + + test.setup() + + local fs_tree = test.fs_tree + + after_each(function() + u.clear_environment() + end) + + describe("with reveal:", function() + it("`:Neotree float reveal` should reveal the current file in the floating window", function() + local cmd = "Neotree float reveal" + local testfile = fs_tree.lookup["./foo/bar/baz1.txt"].abspath + u.editfile(testfile) + run_focus_command(cmd, testfile) + end) + + it("`:Neotree reveal toggle` should toggle the reveal-state of the tree", function() + local cmd = "Neotree reveal toggle" + local testfile = fs_tree.lookup["./foo/foofile1.txt"].abspath + u.editfile(testfile) + + -- toggle OPEN + run_focus_command(cmd, testfile) + local tree_winid = vim.api.nvim_get_current_win() + + -- toggle CLOSE + run_close_command(cmd) + verify.window_handle_is_not(tree_winid) + verify.buf_name_is(testfile) + + -- toggle OPEN with a different file + testfile = fs_tree.lookup["./foo/bar/baz1.txt"].abspath + u.editfile(testfile) + run_focus_command(cmd, testfile) + end) + + it( + "`:Neotree float reveal toggle` should toggle the reveal-state of the floating window", + function() + local cmd = "Neotree float reveal toggle" + local testfile = fs_tree.lookup["./foo/foofile1.txt"].abspath + u.editfile(testfile) + + -- toggle OPEN + run_focus_command(cmd, testfile) + local tree_winid = vim.api.nvim_get_current_win() + + -- toggle CLOSE + run_close_command("Neotree float reveal toggle") + verify.window_handle_is_not(tree_winid) + verify.buf_name_is(testfile) + + -- toggle OPEN + testfile = fs_tree.lookup["./foo/bar/baz2.txt"].abspath + u.editfile(testfile) + run_focus_command(cmd, testfile) + end + ) + + it("`:Neotree reveal` should reveal the current file in the sidebar", function() + local cmd = "Neotree reveal" + local testfile = fs_tree.lookup["topfile1"].abspath + u.editfile(testfile) + run_focus_command(cmd, testfile) + end) + end) + + for _, follow_current_file in ipairs({ true, false }) do + require("neo-tree").setup({ + filesystem = { + follow_current_file = { + enabled = follow_current_file, + }, + }, + }) + + describe(string.format("w/ follow_current_file.enabled=%s", follow_current_file), function() + describe("with show :", function() + it("`:Neotree show` should show the window without focusing", function() + local cmd = "Neotree show" + local testfile = fs_tree.lookup["topfile1"].abspath + u.editfile(testfile) + run_show_command(cmd) + end) + + it("`:Neotree show toggle` should retain the focused node on next show", function() + local cmd = "Neotree show toggle" + local topfile = fs_tree.lookup["topfile1"].abspath + local baz = fs_tree.lookup["./foo/bar/baz1.txt"].abspath + + -- focus a sub node to see if state is retained + u.editfile(baz) + run_focus_command(":Neotree reveal", baz) + local expected_tree_node = baz + + verify.after(500, function() + -- toggle CLOSE + run_close_command(cmd) + + -- toggle OPEN + u.editfile(topfile) + if follow_current_file then + expected_tree_node = topfile + end + run_show_command(cmd, expected_tree_node) + return true + end) + end) + end) + + describe("with focus :", function() + it("`:Neotree focus` should show the window and focus it", function() + local cmd = "Neotree focus" + local testfile = fs_tree.lookup["topfile1"].abspath + u.editfile(testfile) + run_focus_command(cmd) + end) + + it("`:Neotree focus toggle` should retain the focused node on next focus", function() + local cmd = "Neotree focus toggle" + local topfile = fs_tree.lookup["topfile1"].abspath + local baz = fs_tree.lookup["./foo/bar/baz1.txt"].abspath + + -- focus a sub node to see if state is retained + u.editfile(baz) + run_focus_command("Neotree reveal", baz) + local expected_tree_node = baz + -- toggle CLOSE + run_close_command(cmd) + + verify.after(500, function() + -- toggle OPEN + u.editfile(topfile) + if follow_current_file then + expected_tree_node = topfile + end + run_focus_command(cmd, expected_tree_node) + return true + end) + end) + end) + end) + end + + test.teardown() +end) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/events/queue_spec.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/events/queue_spec.lua new file mode 100644 index 0000000..a3a6c64 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/events/queue_spec.lua @@ -0,0 +1,28 @@ +pcall(require, "luacov") + +describe("Event queue", function() + it("should return data when handled = true", function() + local events = require("neo-tree.events") + events.subscribe({ + event = "test", + handler = function() + return { data = "first" } + end, + }) + events.subscribe({ + event = "test", + handler = function() + return { handled = true, data = "second" } + end, + }) + events.subscribe({ + event = "test", + handler = function() + return { data = "third" } + end, + }) + local result = events.fire_event("test") or {} + local data = result.data + assert.are.same("second", data) + end) +end) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/hacks/hacks_spec.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/hacks/hacks_spec.lua new file mode 100644 index 0000000..8a13e70 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/hacks/hacks_spec.lua @@ -0,0 +1,33 @@ +local u = require("tests.utils") +local verify = require("tests.utils.verify") +describe("Opening buffers in neo-tree window", function() + -- Just make sure we start all tests in the expected state + before_each(function() + u.eq(1, #vim.api.nvim_list_wins()) + u.eq(1, #vim.api.nvim_list_tabpages()) + end) + + after_each(function() + u.clear_environment() + end) + + local width = 33 + describe("should automatically redirect to other buffers", function() + it("without changing our own width", function() + require("neo-tree").setup({ + window = { + width = width, + }, + }) + vim.cmd("e test.txt") + vim.cmd("Neotree") + local neotree = vim.api.nvim_get_current_win() + assert.are.equal(width, vim.api.nvim_win_get_width(neotree)) + + vim.cmd("bnext") + verify.schedule(function() + return assert.are.equal(width, vim.api.nvim_win_get_width(neotree)) + end, nil, "width should remain 33") + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/keymap/normalization_spec.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/keymap/normalization_spec.lua new file mode 100644 index 0000000..96f9500 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/keymap/normalization_spec.lua @@ -0,0 +1,43 @@ +local helper = require("neo-tree.setup.mapping-helper") +describe("keymap normalization", function() + it("passes basic tests", function() + local tests = { + { "", "" }, + { "", "" }, + { "", "" }, + { "", "" }, + { "", "" }, + { "", "" }, + { "", "" }, + { "", "" }, + } + for _, test in ipairs(tests) do + local key = helper.normalize_map_key(test[1]) + assert(key == test[2], string.format("%s != %s", key, test[2])) + end + end) + it("allows for proper merging", function() + local defaults = helper.normalize_mappings({ + ["n"] = "n", + [""] = "escape", + [""] = "j", + [""] = "capital_j", + ["a"] = "keep_this", + }) + local new = helper.normalize_mappings({ + ["n"] = "n", + [""] = "escape", + [""] = "j", + ["b"] = "override_this", + }) + local merged = vim.tbl_deep_extend("force", defaults, new) + assert.are.same({ + ["n"] = "n", + [""] = "escape", + [""] = "j", + [""] = "capital_j", + ["a"] = "keep_this", + ["b"] = "override_this", + }, merged) + end) +end) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/manager/state_spec.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/manager/state_spec.lua new file mode 100644 index 0000000..a632096 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/manager/state_spec.lua @@ -0,0 +1,8 @@ +describe("manager state", function() + it("can be retrieved at startup", function() + local fs_state = require("neo-tree.sources.manager").get_state("filesystem") + local buffers_state = require("neo-tree.sources.manager").get_state("buffers") + assert.are_equal(type(fs_state), "table") + assert.are_equal(type(buffers_state), "table") + end) +end) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/qol_spec.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/qol_spec.lua new file mode 100644 index 0000000..335ed20 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/qol_spec.lua @@ -0,0 +1,25 @@ +local u = require("tests.utils") +local verify = require("tests.utils.verify") +describe("Neo-tree should be able to track previous windows", function() + -- Just make sure we start all tests in the expected state + before_each(function() + u.eq(1, #vim.api.nvim_list_wins()) + u.eq(1, #vim.api.nvim_list_tabpages()) + end) + + after_each(function() + u.clear_environment() + end) + + it("before opening", function() + vim.cmd.vsplit() + vim.cmd.split() + vim.cmd.wincmd("l") + local win = vim.api.nvim_get_current_win() + verify.schedule(function() + local prior_windows = + require("neo-tree.utils").prior_windows[vim.api.nvim_get_current_tabpage()] + return assert.are.same(win, prior_windows[#prior_windows]) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/sources/container_spec.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/sources/container_spec.lua new file mode 100644 index 0000000..9fc735b --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/sources/container_spec.lua @@ -0,0 +1,146 @@ +pcall(require, "luacov") + +local ns_id = require("neo-tree.ui.highlights").ns_id +local u = require("tests.utils") + +local config = { + renderers = { + directory = { + { + "container", + content = { + { "indent", zindex = 10 }, + { "icon", zindex = 10 }, + { "name", zindex = 10 }, + { "name", zindex = 5, align = "right" }, + }, + }, + }, + file = { + { + "container", + content = { + { "indent", zindex = 10 }, + { "icon", zindex = 10 }, + { "name", zindex = 10 }, + { "name", zindex = 20, align = "right" }, + }, + }, + }, + }, + window = { + width = 40, + }, +} + +local config_right = { + renderers = { + directory = { + { + "container", + enable_character_fade = false, + content = { + { "indent", zindex = 10, align = "right" }, + { "icon", zindex = 10, align = "right" }, + { "name", zindex = 10, align = "right" }, + }, + }, + }, + file = { + { + "container", + enable_character_fade = false, + content = { + { "indent", zindex = 10, align = "right" }, + { "icon", zindex = 10, align = "right" }, + { "name", zindex = 10, align = "right" }, + }, + }, + }, + }, + window = { + width = 40, + }, +} + +local test_dir = { + items = { + { + name = "foo", + type = "dir", + items = { + { + name = "bar", + type = "dir", + items = { + { name = "bar1.txt", type = "file" }, + { name = "bar2.txt", type = "file" }, + }, + }, + { name = "foo1.lua", type = "file" }, + }, + }, + { name = "bazbazbazbazbazbazbazbazbazbazbazbazbazbazbazbazbaz", type = "dir" }, + { name = "1.md", type = "file" }, + }, +} + +describe("sources/components/container", function() + local req_switch = u.get_require_switch() + + local test = u.fs.init_test(test_dir) + test.setup() + + after_each(function() + if req_switch then + req_switch.restore() + end + + u.clear_environment() + end) + + describe("should expand to width", function() + for pow = 4, 8 do + it(2 ^ pow, function() + config.window.width = 2 ^ pow + require("neo-tree").setup(config) + vim.cmd([[Neotree focus]]) + u.wait_for(function() + return vim.bo.filetype == "neo-tree" + end) + + assert.equals(vim.bo.filetype, "neo-tree") + + local width = vim.api.nvim_win_get_width(0) + local lines = vim.api.nvim_buf_get_lines(0, 2, -1, false) + for _, line in ipairs(lines) do + assert.is_true(#line >= width) + end + end) + end + end) + + describe("right-align should matches width", function() + for pow = 4, 8 do + it(2 ^ pow, function() + config_right.window.width = 2 ^ pow + require("neo-tree").setup(config_right) + vim.cmd([[Neotree focus]]) + u.wait_for(function() + return vim.bo.filetype == "neo-tree" + end) + + assert.equals(vim.bo.filetype, "neo-tree") + + local width = vim.api.nvim_win_get_width(0) + local lines = vim.api.nvim_buf_get_lines(0, 1, -1, false) + for _, line in ipairs(lines) do + line = vim.fn.trim(line, " ", 2) + assert.equals(width, vim.fn.strchars(line)) + end + end) + end + end) + + test.teardown() +end) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/sources/filesystem/filesystem_netrw_hijack_spec.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/sources/filesystem/filesystem_netrw_hijack_spec.lua new file mode 100644 index 0000000..cfe8026 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/sources/filesystem/filesystem_netrw_hijack_spec.lua @@ -0,0 +1,89 @@ +pcall(require, "luacov") + +local u = require("tests.utils") +local verify = require("tests.utils.verify") + +describe("Filesystem netrw hijack", function() + after_each(function() + u.clear_environment() + end) + + it("does not interfere with netrw when disabled", function() + require("neo-tree").setup({ + filesystem = { + hijack_netrw_behavior = "disabled", + window = { + position = "left", + }, + }, + }) + + vim.cmd("edit .") + + assert(#vim.api.nvim_list_wins() == 1, "there should only be one window") + + verify.after(100, function() + local name = vim.api.nvim_buf_get_name(0) + return name ~= "neo-tree filesystem [1]" + end, "the buffer should not be neo-tree") + end) + + it("opens in sidebar when behavior is open_default", function() + local file = "Makefile" + vim.cmd("edit " .. file) + + require("neo-tree").setup({ + filesystem = { + hijack_netrw_behavior = "open_default", + window = { + position = "left", + }, + }, + }) + + vim.cmd("edit .") + + verify.eventually(200, function() + return #vim.api.nvim_list_wins() == 2 + end, "there should be two windows") + + verify.buf_name_endswith("neo-tree filesystem [1]") + + verify.eventually(100, function() + local expected_buf_name = "Makefile" + local buf_at_2 = vim.api.nvim_win_get_buf(vim.fn.win_getid(2)) + local name_at_2 = vim.api.nvim_buf_get_name(buf_at_2) + if name_at_2:sub(-#expected_buf_name) == expected_buf_name then + return true + else + return false + end + end, file .. " is not at window 2") + end) + + -- This test is flaky and usually fails in github actions but not always + -- so I'm disabling it for now. + -- TODO: fix this test + -- + --it("opens in in splits when behavior is open_current", function() + -- local file = "Makefile" + -- vim.cmd("edit " .. file) + + -- require("neo-tree").setup({ + -- filesystem = { + -- hijack_netrw_behavior = "open_current", + -- }, + -- }) + + -- assert(#vim.api.nvim_list_wins() == 1, "Test should start with one window") + + -- vim.cmd("split .") + + -- verify.eventually(200, function() + -- if #vim.api.nvim_list_wins() ~= 2 then + -- return false + -- end + -- return vim.bo[0].filetype == "neo-tree" + -- end, "neotree is not in the second window") + --end) +end) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/sources/manager_spec.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/sources/manager_spec.lua new file mode 100644 index 0000000..f11f908 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/sources/manager_spec.lua @@ -0,0 +1,124 @@ +pcall(require, "luacov") + +local u = require("tests.utils") +local verify = require("tests.utils.verify") + +local manager = require("neo-tree.sources.manager") + +local get_dirs = function(winid) + winid = winid or vim.api.nvim_get_current_win() + local tabnr = vim.api.nvim_tabpage_get_number(vim.api.nvim_win_get_tabpage(winid)) + local winnr = vim.api.nvim_win_get_number(winid) + return { + win = vim.fn.getcwd(winnr), + tab = vim.fn.getcwd(-1, tabnr), + global = vim.fn.getcwd(-1, -1), + } +end + +local get_state_for_tab = function(tabid) + for _, state in ipairs(manager._get_all_states()) do + if state.tabid == tabid then + return state + end + end +end + +local get_tabnr = function(tabid) + return vim.api.nvim_tabpage_get_number(tabid or vim.api.nvim_get_current_tabpage()) +end + +describe("Manager", function() + local test = u.fs.init_test({ + items = { + { + name = "foo", + type = "dir", + items = { + { name = "foofile1.txt", type = "file" }, + }, + }, + { name = "topfile1.txt", type = "file", id = "topfile1" }, + }, + }) + + test.setup() + + local fs_tree = test.fs_tree + + -- Just make sure we start all tests in the expected state + before_each(function() + u.eq(1, #vim.api.nvim_list_wins()) + u.eq(1, #vim.api.nvim_list_tabpages()) + vim.cmd.lcd(fs_tree.abspath) + vim.cmd.tcd(fs_tree.abspath) + vim.cmd.cd(fs_tree.abspath) + end) + + after_each(function() + u.clear_environment() + end) + + local setup_2_tabs = function() + -- create 2 tabs + local tab1 = vim.api.nvim_get_current_tabpage() + local win1 = vim.api.nvim_get_current_win() + vim.cmd.tabnew() + local tab2 = vim.api.nvim_get_current_tabpage() + local win2 = vim.api.nvim_get_current_win() + u.neq(tab2, tab1) + u.neq(win2, win1) + + -- set different directories + vim.api.nvim_set_current_tabpage(tab2) + local base_dir = vim.fn.getcwd() + vim.cmd.tcd("foo") + local new_dir = vim.fn.getcwd() + + -- open neo-tree + vim.api.nvim_set_current_tabpage(tab1) + vim.cmd.Neotree("show") + vim.api.nvim_set_current_tabpage(tab2) + vim.cmd.Neotree("show") + + return { + tab1 = tab1, + tab2 = tab2, + win1 = win1, + win2 = win2, + tab1_dir = base_dir, + tab2_dir = new_dir, + } + end + + it("should respect changed tab cwd", function() + local ctx = setup_2_tabs() + + local state1 = get_state_for_tab(ctx.tab1) + local state2 = get_state_for_tab(ctx.tab2) + u.eq(ctx.tab1_dir, manager.get_cwd(state1)) + u.eq(ctx.tab2_dir, manager.get_cwd(state2)) + end) + + it("should have correct tab cwd after tabs order is changed", function() + local ctx = setup_2_tabs() + + -- tab numbers should be the same as ids + u.eq(1, get_tabnr(ctx.tab1)) + u.eq(2, get_tabnr(ctx.tab2)) + + -- swap tabs + vim.cmd.tabfirst() + vim.cmd.tabmove("+1") + + -- make sure tabs have been swapped + u.eq(2, get_tabnr(ctx.tab1)) + u.eq(1, get_tabnr(ctx.tab2)) + + -- verify that tab dirs are the same as nvim tab cwd + local state1 = get_state_for_tab(ctx.tab1) + local state2 = get_state_for_tab(ctx.tab2) + u.eq(get_dirs(ctx.win1).tab, manager.get_cwd(state1)) + u.eq(get_dirs(ctx.win2).tab, manager.get_cwd(state2)) + end) +end) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/sources/navigate_spec.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/sources/navigate_spec.lua new file mode 100644 index 0000000..283f77f --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/sources/navigate_spec.lua @@ -0,0 +1,51 @@ +pcall(require, "luacov") + +local uv = vim.uv or vim.loop + +---Return all sources inside "lua/neo-tree/sources" +---@return string[] # name of sources found +local function find_all_sources() + local base_dir = "lua/neo-tree/sources" + local result = {} + local fd = uv.fs_scandir(base_dir) + while fd do + local name, typ = uv.fs_scandir_next(fd) + if not name then + break + end + if typ == "directory" then + local ok, mod = pcall(require, "neo-tree.sources." .. name) + if ok and mod.name then + result[#result + 1] = name + end + end + end + return result +end + +describe("sources.navigate(...: #)", function() + it("neo-tree.sources.filesystem.navigate exists", function() + local ok, mod = pcall(require, "neo-tree.sources.filesystem") + assert.is_true(ok) + assert.not_nil(mod.navigate) + end) + local filesystem_navigate_nparams = + debug.getinfo(require("neo-tree.sources.filesystem").navigate).nparams + it("neo-tree.sources.filesystem.navigate is a func and has args", function() + assert.not_nil(filesystem_navigate_nparams) + assert.is_true(filesystem_navigate_nparams > 0) + end) + for _, source in ipairs(find_all_sources()) do + describe(string.format("Test: %s.navigate", source), function() + it(source .. ".navigate is able to require and exists", function() + local ok, mod = pcall(require, "neo-tree.sources." .. source) + assert.is_true(ok) + assert.not_nil(mod.navigate) + end) + it(source .. ".navigate has same num of args as filesystem", function() + local nparams = debug.getinfo(require("neo-tree.sources." .. source).navigate).nparams + assert.are.equal(filesystem_navigate_nparams, nparams) + end) + end) + end +end) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/ui/icons_spec.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/ui/icons_spec.lua new file mode 100644 index 0000000..d74df30 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/ui/icons_spec.lua @@ -0,0 +1,227 @@ +pcall(require, "luacov") + +local ns_id = require("neo-tree.ui.highlights").ns_id +local u = require("tests.utils") + +describe("ui/icons", function() + local req_switch = u.get_require_switch() + + local test = u.fs.init_test({ + items = { + { + name = "foo", + type = "dir", + items = { + { + name = "bar", + type = "dir", + items = { + { name = "bar1.txt", type = "file" }, + { name = "bar2.txt", type = "file" }, + }, + }, + { name = "foo1.lua", type = "file" }, + }, + }, + { name = "baz", type = "dir" }, + { name = "1.md", type = "file" }, + }, + }) + + test.setup() + + local fs_tree = test.fs_tree + + after_each(function() + if req_switch then + req_switch.restore() + end + + u.clear_environment() + end) + + describe("w/ default_config", function() + before_each(function() + require("neo-tree").setup({}) + end) + + it("works w/o nvim-web-devicons", function() + req_switch.disable_package("nvim-web-devicons") + + vim.cmd([[:Neotree focus]]) + u.wait_for_neo_tree() + + local winid = vim.api.nvim_get_current_win() + local bufnr = vim.api.nvim_win_get_buf(winid) + + u.assert_buf_lines(bufnr, { + string.format("  %s", fs_tree.abspath):sub(1, 42), + "  baz", + "  foo", + " * 1.md", + }) + + vim.api.nvim_win_set_cursor(winid, { 2, 0 }) + u.feedkeys("") + + vim.api.nvim_win_set_cursor(winid, { 3, 0 }) + u.feedkeys("") + + vim.wait(100) + + u.assert_buf_lines(bufnr, { + string.format("  %s", fs_tree.abspath):sub(1, 42), + " 󰉖 baz", + "  foo", + " │  bar", + " └ * foo1.lua", + " * 1.md", + }) + + u.assert_highlight(bufnr, ns_id, 1, " ", "NeoTreeDirectoryIcon") + u.assert_highlight(bufnr, ns_id, 2, "󰉖 ", "NeoTreeDirectoryIcon") + u.assert_highlight(bufnr, ns_id, 4, " ", "NeoTreeDirectoryIcon") + u.assert_highlight(bufnr, ns_id, 5, "* ", "NeoTreeFileIcon") + end) + + it("works w/ nvim-web-devicons", function() + vim.cmd([[:Neotree focus]]) + u.wait_for_neo_tree() + + local winid = vim.api.nvim_get_current_win() + local bufnr = vim.api.nvim_win_get_buf(winid) + + u.assert_buf_lines(bufnr, { + vim.fn.strcharpart(string.format("  %s", fs_tree.abspath), 0, 40), + "  baz", + "  foo", + "  1.md", + }) + + vim.api.nvim_win_set_cursor(winid, { 2, 0 }) + u.feedkeys("") + + vim.api.nvim_win_set_cursor(winid, { 3, 0 }) + u.feedkeys("") + + vim.wait(100) + + u.assert_buf_lines(bufnr, { + vim.fn.strcharpart(string.format("  %s", fs_tree.abspath), 0, 40), + " 󰉖 baz", + "  foo", + " │  bar", + " └  foo1.lua", + "  1.md", + }) + + u.assert_highlight(bufnr, ns_id, 1, " ", "NeoTreeDirectoryIcon") + u.assert_highlight(bufnr, ns_id, 2, "󰉖 ", "NeoTreeDirectoryIcon") + u.assert_highlight(bufnr, ns_id, 4, " ", "NeoTreeDirectoryIcon") + + local extmarks = u.get_text_extmarks(bufnr, ns_id, 5, " ") + u.eq(#extmarks, 1) + u.neq(extmarks[1][4].hl_group, "NeoTreeFileIcon") + end) + end) + + describe("custom config", function() + local config + before_each(function() + config = { + default_component_configs = { + icon = { + folder_closed = "c", + folder_open = "o", + folder_empty = "e", + default = "f", + highlight = "TestNeoTreeFileIcon", + }, + }, + } + + require("neo-tree").setup(config) + end) + + it("works w/o nvim-web-devicons", function() + req_switch.disable_package("nvim-web-devicons") + + vim.cmd([[:Neotree focus]]) + u.wait_for_neo_tree() + + local winid = vim.api.nvim_get_current_win() + local bufnr = vim.api.nvim_win_get_buf(winid) + + u.assert_buf_lines(bufnr, { + string.format(" o %s", fs_tree.abspath):sub(1, 40), + " c baz", + " c foo", + " f 1.md", + }) + + vim.api.nvim_win_set_cursor(winid, { 2, 0 }) + u.feedkeys("") + + vim.api.nvim_win_set_cursor(winid, { 3, 0 }) + u.feedkeys("") + + vim.wait(100) + + u.assert_buf_lines(bufnr, { + string.format(" o %s", fs_tree.abspath):sub(1, 40), + " e baz", + " o foo", + " │ c bar", + " └ f foo1.lua", + " f 1.md", + }) + + u.assert_highlight(bufnr, ns_id, 1, "o ", "NeoTreeDirectoryIcon") + u.assert_highlight(bufnr, ns_id, 2, "e ", "NeoTreeDirectoryIcon") + u.assert_highlight(bufnr, ns_id, 4, "c ", "NeoTreeDirectoryIcon") + u.assert_highlight(bufnr, ns_id, 5, "f ", config.default_component_configs.icon.highlight) + end) + + it("works w/ nvim-web-devicons", function() + vim.cmd([[:Neotree focus]]) + u.wait_for_neo_tree() + + local winid = vim.api.nvim_get_current_win() + local bufnr = vim.api.nvim_win_get_buf(winid) + + u.assert_buf_lines(bufnr, { + vim.fn.strcharpart(string.format(" o %s", fs_tree.abspath), 0, 40), + " c baz", + " c foo", + "  1.md", + }) + + vim.api.nvim_win_set_cursor(winid, { 2, 0 }) + u.feedkeys("") + + vim.api.nvim_win_set_cursor(winid, { 3, 0 }) + u.feedkeys("") + + vim.wait(100) + + u.assert_buf_lines(bufnr, { + vim.fn.strcharpart(string.format(" o %s", fs_tree.abspath), 0, 40), + " e baz", + " o foo", + " │ c bar", + " └  foo1.lua", + "  1.md", + }) + + u.assert_highlight(bufnr, ns_id, 1, "o ", "NeoTreeDirectoryIcon") + u.assert_highlight(bufnr, ns_id, 2, "e ", "NeoTreeDirectoryIcon") + u.assert_highlight(bufnr, ns_id, 4, "c ", "NeoTreeDirectoryIcon") + + local extmarks = u.get_text_extmarks(bufnr, ns_id, 5, " ") + u.eq(#extmarks, 1) + u.neq(extmarks[1][4].hl_group, config.default_component_configs.icon.highlight) + end) + end) + + test.teardown() +end) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/utils/path_spec.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/utils/path_spec.lua new file mode 100644 index 0000000..f4ddd75 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/neo-tree/utils/path_spec.lua @@ -0,0 +1,66 @@ +pcall(require, "luacov") +local utils = require("neo-tree.utils") + +describe("is_subpath", function() + local common_tests = function() + -- Relative paths + assert.are.same(true, utils.is_subpath("a", "a/subpath")) + assert.are.same(false, utils.is_subpath("a", "b/c")) + assert.are.same(false, utils.is_subpath("a", "b")) + end + it("should work with unix paths", function() + local old = utils.is_windows + utils.is_windows = false + common_tests() + assert.are.same(true, utils.is_subpath("/a", "/a/subpath")) + assert.are.same(false, utils.is_subpath("/a", "/b/c")) + + -- Edge cases + assert.are.same(false, utils.is_subpath("", "")) + assert.are.same(true, utils.is_subpath("/", "/")) + + -- Paths with trailing slashes + assert.are.same(true, utils.is_subpath("/a/", "/a/subpath")) + assert.are.same(true, utils.is_subpath("/a/", "/a/subpath/")) + assert.are.same(true, utils.is_subpath("/a", "/a/subpath")) + assert.are.same(true, utils.is_subpath("/a", "/a/subpath/")) + + -- Paths with different casing + assert.are.same(true, utils.is_subpath("/TeSt", "/TeSt/subpath")) + assert.are.same(false, utils.is_subpath("/A", "/a/subpath")) + assert.are.same(false, utils.is_subpath("/A", "/a/subpath")) + utils.is_windows = old + end) + it("should work on windows paths", function() + local old = utils.is_windows + utils.is_windows = true + common_tests() + assert.are.same(true, utils.is_subpath("C:", "C:")) + assert.are.same(false, utils.is_subpath("C:", "D:")) + assert.are.same(true, utils.is_subpath("C:/A", [[C:\A]])) + + -- Test Windows paths with backslashes + assert.are.same(true, utils.is_subpath([[C:\Users\user]], [[C:\Users\user\Documents]])) + assert.are.same(false, utils.is_subpath([[C:\Users\user]], [[D:\Users\user]])) + assert.are.same(false, utils.is_subpath([[C:\Users\user]], [[C:\Users\usera]])) + + -- Test Windows paths with forward slashes + assert.are.same(true, utils.is_subpath("C:/Users/user", "C:/Users/user/Documents")) + assert.are.same(false, utils.is_subpath("C:/Users/user", "D:/Users/user")) + assert.are.same(false, utils.is_subpath("C:/Users/user", "C:/Users/usera")) + + -- Test Windows paths with drive letters + assert.are.same(true, utils.is_subpath("C:", "C:/Users/user")) + assert.are.same(false, utils.is_subpath("C:", "D:/Users/user")) + + -- Test Windows paths with UNC paths + assert.are.same(true, utils.is_subpath([[\\server\share]], [[\\server\share\folder]])) + assert.are.same(false, utils.is_subpath([[\\server\share]], [[\\server2\share]])) + + -- Test Windows paths with trailing backslashes + assert.are.same(true, utils.is_subpath([[C:\Users\user\]], [[C:\Users\user\Documents]])) + assert.are.same(true, utils.is_subpath("C:/Users/user/", "C:/Users/user/Documents")) + + utils.is_windows = old + end) +end) diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/utils/fs.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/utils/fs.lua new file mode 100644 index 0000000..66e35a8 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/utils/fs.lua @@ -0,0 +1,94 @@ +local Path = require("plenary.path") + +local fs = {} + +function fs.create_temp_dir() + -- Resolve for two reasons. + -- 1. Follow any symlinks which make comparing paths fail. (on macOS, TMPDIR can be under /var which is symlinked to + -- /private/var) + -- 2. Remove any double separators (on macOS TMPDIR can end in a trailing / which absolute doesn't remove, this should + -- be coverted by https://github.com/nvim-lua/plenary.nvim/issues/330). + local temp_dir = vim.fn.resolve( + Path:new( + vim.fn.fnamemodify(vim.fn.tempname(), ":h"), + string.format("neo-tree-test-%s", vim.fn.rand()) + ):absolute() + ) + vim.fn.mkdir(temp_dir, "p") + return temp_dir +end + +function fs.create_dir(path) + local abspath = Path:new(path):absolute() + vim.fn.mkdir(abspath, "p") +end + +function fs.remove_dir(dir, recursive) + if vim.fn.isdirectory(dir) == 1 then + return vim.fn.delete(dir, recursive and "rf" or "d") == 0 + end + return false +end + +function fs.write_file(path, content) + local abspath = Path:new(path):absolute() + fs.create_dir(vim.fn.fnamemodify(abspath, ":h")) + vim.fn.writefile(content or {}, abspath) +end + +function fs.create_fs_tree(fs_tree) + local function create_items(items, basedir, relative_root_path) + relative_root_path = relative_root_path or "." + + for _, item in ipairs(items) do + local relative_path = relative_root_path .. "/" .. item.name + + -- create lookups + fs_tree.lookup[relative_path] = item + if item.id then + fs_tree.lookup[item.id] = item + end + + -- create actual files and directories + if item.type == "dir" then + item.abspath = Path:new(basedir, item.name):absolute() + fs.create_dir(item.abspath) + if item.items then + create_items(item.items, item.abspath, relative_path) + end + elseif item.type == "file" then + item.abspath = Path:new(basedir, item.name):absolute() + fs.write_file(item.abspath) + end + end + end + + create_items(fs_tree.items, fs_tree.abspath) + + return fs_tree +end + +function fs.init_test(fs_tree) + fs_tree.lookup = {} + if not fs_tree.abspath then + fs_tree.abspath = fs.create_temp_dir() + end + + local function setup() + fs.remove_dir(fs_tree.abspath, true) + fs.create_fs_tree(fs_tree) + vim.cmd("tcd " .. fs_tree.abspath) + end + + local function teardown() + fs.remove_dir(fs_tree.abspath, true) + end + + return { + fs_tree = fs_tree, + setup = setup, + teardown = teardown, + } +end + +return fs diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/utils/init.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/utils/init.lua new file mode 100644 index 0000000..ce69645 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/utils/init.lua @@ -0,0 +1,191 @@ +local mod = { + fs = require("tests.utils.fs"), +} + +function mod.clear_environment() + -- Create fresh window + vim.cmd("top new | wincmd o") + local keepbufnr = vim.api.nvim_get_current_buf() + -- Clear ALL neo-tree state + require("neo-tree.sources.manager")._clear_state() + -- Cleanup any remaining buffers + for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do + if bufnr ~= keepbufnr then + vim.api.nvim_buf_delete(bufnr, { force = true }) + end + end + assert(#vim.api.nvim_tabpage_list_wins(0) == 1, "Failed to properly clear tab") + assert(#vim.api.nvim_list_bufs() == 1, "Failed to properly clear buffers") +end + +mod.editfile = function(testfile) + vim.cmd("e " .. testfile) + assert.are.same( + vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ":p"), + vim.fn.fnamemodify(testfile, ":p") + ) +end + +function mod.eq(...) + return assert.are.same(...) +end + +function mod.neq(...) + return assert["not"].are.same(...) +end + +---@param keys string +---@param mode? string +function mod.feedkeys(keys, mode) + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(keys, true, false, true), mode or "x", true) +end + +---@param tbl table +---@param keys string[] +function mod.tbl_pick(tbl, keys) + if not keys or #keys == 0 then + return tbl + end + + local new_tbl = {} + for _, key in ipairs(keys) do + new_tbl[key] = tbl[key] + end + return new_tbl +end + +local orig_require = _G.require +-- can be used to enable/disable package +-- for specific tests +function mod.get_require_switch() + local disabled_packages = {} + + local function fake_require(name) + if vim.tbl_contains(disabled_packages, name) then + return error("test: package disabled") + end + + return orig_require(name) + end + + return { + disable_package = function(name) + _G.require = fake_require + package.loaded[name] = nil + table.insert(disabled_packages, name) + end, + enable_package = function(name) + _G.require = fake_require + disabled_packages = vim.tbl_filter(function(package_name) + return package_name ~= name + end, disabled_packages) + end, + restore = function() + disabled_packages = {} + _G.require = orig_require + end, + } +end + +---@param bufnr number +---@param lines string[] +---@param linenr_start? integer (1-indexed) +---@param linenr_end? integer (1-indexed, inclusive) +function mod.assert_buf_lines(bufnr, lines, linenr_start, linenr_end) + mod.eq( + lines, + vim.api.nvim_buf_get_lines( + bufnr, + linenr_start and linenr_start - 1 or 0, + linenr_end or -1, + false + ) + ) +end + +---@param bufnr number +---@param ns_id integer +---@param linenr integer (1-indexed) +---@param byte_start? integer (0-indexed) +---@param byte_end? integer (0-indexed, inclusive) +function mod.get_line_extmarks(bufnr, ns_id, linenr, byte_start, byte_end) + return vim.api.nvim_buf_get_extmarks( + bufnr, + ns_id, + { linenr - 1, byte_start or 0 }, + { linenr - 1, byte_end and byte_end + 1 or -1 }, + { details = true } + ) +end + +---@param bufnr number +---@param ns_id integer +---@param linenr integer (1-indexed) +---@param text string +---@return table[] +---@return { byte_start: integer, byte_end: integer } info (byte range: 0-indexed, inclusive) +function mod.get_text_extmarks(bufnr, ns_id, linenr, text) + local line = vim.api.nvim_buf_get_lines(bufnr, linenr - 1, linenr, false)[1] + + local byte_start = string.find(line, text) -- 1-indexed + byte_start = byte_start - 1 -- 0-indexed + local byte_end = byte_start + #text - 1 -- inclusive + + local extmarks = vim.api.nvim_buf_get_extmarks( + bufnr, + ns_id, + { linenr - 1, byte_start }, + { linenr - 1, byte_end }, + { details = true } + ) + + return extmarks, { byte_start = byte_start, byte_end = byte_end } +end + +---@param extmark table +---@param linenr number (1-indexed) +---@param text string +---@param hl_group string +function mod.assert_extmark(extmark, linenr, text, hl_group) + mod.eq(extmark[2], linenr - 1) + + if text then + local start_col = extmark[3] + mod.eq(extmark[4].end_col - start_col, #text) + end + + mod.eq(mod.tbl_pick(extmark[4], { "end_row", "hl_group" }), { + end_row = linenr - 1, + hl_group = hl_group, + }) +end + +---@param bufnr number +---@param ns_id integer +---@param linenr integer (1-indexed) +---@param text string +---@param hl_group string +function mod.assert_highlight(bufnr, ns_id, linenr, text, hl_group) + local extmarks, info = mod.get_text_extmarks(bufnr, ns_id, linenr, text) + + mod.eq(#extmarks, 1) + mod.eq(extmarks[1][3], info.byte_start) + mod.assert_extmark(extmarks[1], linenr, text, hl_group) +end + +---@param callback fun(): boolean +---@param options? { interval?: integer, timeout?: integer } +function mod.wait_for(callback, options) + options = options or {} + vim.wait(options.timeout or 1000, callback, options.interval or 100) +end + +---@param options? { interval?: integer, timeout?: integer } +function mod.wait_for_neo_tree(options) + local verify = require("tests.utils.verify") + mod.wait_for(function() + return verify.get_state() ~= nil + end, options) +end + +return mod diff --git a/.config/nvim/pack/tree/start/neo-tree.nvim/tests/utils/verify.lua b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/utils/verify.lua new file mode 100644 index 0000000..e4cba64 --- /dev/null +++ b/.config/nvim/pack/tree/start/neo-tree.nvim/tests/utils/verify.lua @@ -0,0 +1,145 @@ +local verify = {} + +verify.eventually = function(timeout, assertfunc, failmsg, ...) + local success, args = false, { ... } + vim.wait(timeout or 1000, function() + success = assertfunc(unpack(args)) + return success + end) + assert(success, failmsg) +end + +local id = 0 +---Waits until the next vim.schedule before running assertfunc +verify.schedule = function(assertfunc, timeout, failmsg) + id = id + 1 + local scheduled_func_ran = false + local success = false + local args + vim.schedule(function() + args = { assertfunc() } + success = args[1] + scheduled_func_ran = true + end) + local notimeout, errcode = vim.wait(timeout or 1000, function() + return scheduled_func_ran + end) + assert(success, failmsg) +end + +verify.after = function(timeout, assertfunc, failmsg) + vim.wait(timeout, function() + return false + end) + assert(assertfunc(), failmsg) +end + +verify.bufnr_is = function(bufnr, timeout) + verify.eventually(timeout or 500, function() + return bufnr == vim.api.nvim_get_current_buf() + end, string.format("Current buffer is expected to be '%s' but is not", bufnr)) +end + +verify.bufnr_is_not = function(bufnr, timeout) + verify.eventually(timeout or 500, function() + return bufnr ~= vim.api.nvim_get_current_buf() + end, string.format("Current buffer is '%s' when expected to not be", bufnr)) +end + +verify.buf_name_endswith = function(buf_name, timeout) + verify.eventually( + timeout or 500, + function() + if buf_name == "" then + return true + end + local n = vim.api.nvim_buf_get_name(0) + if n:sub(-#buf_name) == buf_name then + return true + else + return false + end + end, + string.format("Current buffer name is expected to be end with '%s' but it does not", buf_name) + ) +end + +verify.buf_name_is = function(buf_name, timeout) + verify.eventually(timeout or 500, function() + return buf_name == vim.api.nvim_buf_get_name(0) + end, string.format("Current buffer name is expected to be '%s' but is not", buf_name)) +end + +verify.tree_focused = function(timeout) + verify.eventually(timeout or 1000, function() + if not verify.get_state() then + return false + end + return vim.bo[0].filetype == "neo-tree" + end, "Current buffer is not a 'neo-tree' filetype") +end + +verify.get_state = function(source_name, winid) + if source_name == nil then + local success + success, source_name = pcall(vim.api.nvim_buf_get_var, 0, "neo_tree_source") + if not success then + return nil + end + end + local state = require("neo-tree.sources.manager").get_state(source_name, nil, winid) + if not state.tree then + return nil + end + if not state._ready then + return nil + end + return state +end + +verify.tree_node_is = function(source_name, expected_node_id, winid, timeout) + verify.eventually(timeout or 500, function() + local state = verify.get_state(source_name, winid) + if not state then + return false + end + local success, node = pcall(state.tree.get_node, state.tree) + if not success then + return false + end + if not node then + return false + end + local node_id = node:get_id() + if node_id == expected_node_id then + return true + end + return false + end, string.format("Tree node '%s' not focused", expected_node_id)) +end + +verify.filesystem_tree_node_is = function(expected_node_id, winid, timeout) + verify.tree_node_is("filesystem", expected_node_id, winid, timeout) +end + +verify.buffers_tree_node_is = function(expected_node_id, winid, timeout) + verify.tree_node_is("buffers", expected_node_id, winid, timeout) +end + +verify.git_status_tree_node_is = function(expected_node_id, winid, timeout) + verify.tree_node_is("git_status", expected_node_id, winid, timeout) +end + +verify.window_handle_is = function(winid, timeout) + verify.eventually(timeout or 500, function() + return winid == vim.api.nvim_get_current_win() + end, string.format("Current window handle is expected to be '%s' but is not", winid)) +end + +verify.window_handle_is_not = function(winid, timeout) + verify.eventually(timeout or 500, function() + return winid ~= vim.api.nvim_get_current_win() + end, string.format("Current window handle is not expected to be '%s' but it is", winid)) +end + +return verify diff --git a/.config/nvim/pack/tree/start/nui.nvim/.codecov.yml b/.config/nvim/pack/tree/start/nui.nvim/.codecov.yml new file mode 100644 index 0000000..7c4fa59 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/.codecov.yml @@ -0,0 +1,10 @@ +coverage: + status: + project: + default: + informational: true + only_pulls: true + patch: + default: + informational: true + only_pulls: true diff --git a/.config/nvim/pack/tree/start/nui.nvim/.github/workflows/ci.yml b/.config/nvim/pack/tree/start/nui.nvim/.github/workflows/ci.yml new file mode 100644 index 0000000..ae052e2 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/.github/workflows/ci.yml @@ -0,0 +1,123 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint: + name: luacheck + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Get Cache Key + id: luver-cache-key + env: + CI_RUNNER_OS: ${{ runner.os }} + CI_SECRETS_CACHE_VERSION: ${{ secrets.CACHE_VERSION }} + run: | + echo "value=${CI_RUNNER_OS}-luver-${CI_SECRETS_CACHE_VERSION}-$(date -u +%Y-%m-%d)" >> $GITHUB_OUTPUT + shell: bash + - name: Setup Cache + uses: actions/cache@v3 + with: + path: ~/.local/share/luver + key: ${{ steps.luver-cache-key.outputs.value }} + - name: Setup Lua + uses: MunifTanjim/luver-action@v1 + with: + default: 5.1.5 + lua_versions: 5.1.5 + luarocks_versions: 5.1.5:3.8.0 + - name: Setup luacheck + run: | + luarocks install luacheck + - name: Lint + run: ./scripts/lint.sh --no-cache + + format: + name: stylua + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Check Format + uses: JohnnyMorganz/stylua-action@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + version: 0.17.1 + args: --color always --check lua/nui/ tests/ + + test: + name: test + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Get Cache Key + id: luver-cache-key + env: + CI_RUNNER_OS: ${{ runner.os }} + CI_SECRETS_CACHE_VERSION: ${{ secrets.CACHE_VERSION }} + run: | + echo "value=${CI_RUNNER_OS}-luver-${CI_SECRETS_CACHE_VERSION}-$(date -u +%Y-%m-%d)" >> $GITHUB_OUTPUT + shell: bash + - name: Setup Cache + uses: actions/cache@v3 + with: + path: ~/.local/share/luver + key: ${{ steps.luver-cache-key.outputs.value }} + - name: Setup Lua + uses: MunifTanjim/luver-action@v1 + with: + default: 5.1.5 + lua_versions: 5.1.5 + luarocks_versions: 5.1.5:3.8.0 + - name: Setup luacov + run: | + luarocks install luacov + luarocks install luafilesystem + - name: Setup Neovim + uses: MunifTanjim/setup-neovim-action@v1 + with: + tag: nightly + - name: Run Tests + run: | + nvim --version + ./scripts/test.sh + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + verbose: true + + release: + name: release + if: ${{ github.ref == 'refs/heads/main' }} + needs: + - lint + - format + - test + runs-on: ubuntu-22.04 + permissions: + actions: write + contents: write + pull-requests: write + steps: + - uses: google-github-actions/release-please-action@v3 + id: release + with: + release-type: simple + package-name: nui.nvim + bump-minor-pre-major: true + pull-request-title-pattern: "chore: release ${version}" + include-v-in-tag: false + - name: Trigger Publish + if: ${{ steps.release.outputs.release_created }} + env: + GH_TOKEN: ${{ github.token }} + TAG_NAME: ${{ steps.release.outputs.tag_name }} + run: | + gh workflow run --repo ${GITHUB_REPOSITORY} publish.yml -f version=${TAG_NAME} diff --git a/.config/nvim/pack/tree/start/nui.nvim/.github/workflows/publish.yml b/.config/nvim/pack/tree/start/nui.nvim/.github/workflows/publish.yml new file mode 100644 index 0000000..5d5d22b --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/.github/workflows/publish.yml @@ -0,0 +1,33 @@ +name: Publish + +on: + push: + tags: + - "[0-1].[0-9]+.[0-9]+" + workflow_dispatch: + inputs: + version: + description: Version to publish + required: false + type: string + force: + description: Force publish + required: false + default: false + type: boolean + +jobs: + publish: + name: publish + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: LuaRocks Publish + uses: MunifTanjim/luarocks-publish-action@v1 + with: + lua_version: 5.1.5 + luarocks_version: 3.9.1 + version: ${{ inputs.version }} + api_key: ${{ secrets.LUAROCKS_API_KEY }} + force: ${{ inputs.force }} diff --git a/.config/nvim/pack/tree/start/nui.nvim/.gitignore b/.config/nvim/pack/tree/start/nui.nvim/.gitignore new file mode 100644 index 0000000..cf1071a --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/.gitignore @@ -0,0 +1,5 @@ +.luacheckcache + +luacov.*.out + +.tests diff --git a/.config/nvim/pack/tree/start/nui.nvim/.luacheckrc b/.config/nvim/pack/tree/start/nui.nvim/.luacheckrc new file mode 100644 index 0000000..f4c8623 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/.luacheckrc @@ -0,0 +1,16 @@ +cache = ".luacheckcache" +-- https://luacheck.readthedocs.io/en/stable/warnings.html +ignore = { + "211/_.*", + "212/_.*", + "213/_.*", +} +include_files = { "*.luacheckrc", "lua/**/*.lua", "tests/**/*.lua" } +globals = { "vim" } +std = "luajit" + +files["tests/helpers/**/*.lua"] = { + read_globals = { "assert", "describe" }, +} + +-- vim: set filetype=lua : diff --git a/.config/nvim/pack/tree/start/nui.nvim/.luacov b/.config/nvim/pack/tree/start/nui.nvim/.luacov new file mode 100644 index 0000000..6e6f285 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/.luacov @@ -0,0 +1,6 @@ +include = { + "lua%/nui", +} +includeuntestedfiles = { + "lua/nui" +} diff --git a/.config/nvim/pack/tree/start/nui.nvim/.stylua.toml b/.config/nvim/pack/tree/start/nui.nvim/.stylua.toml new file mode 100644 index 0000000..0fd4cb5 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/.stylua.toml @@ -0,0 +1,6 @@ +column_width = 120 +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 2 +quote_style = "AutoPreferDouble" +no_call_parentheses = false diff --git a/.config/nvim/pack/tree/start/nui.nvim/CHANGELOG.md b/.config/nvim/pack/tree/start/nui.nvim/CHANGELOG.md new file mode 100644 index 0000000..5fa02b9 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/CHANGELOG.md @@ -0,0 +1,331 @@ +# Changelog + +## [0.4.0](https://github.com/MunifTanjim/nui.nvim/compare/0.3.0...0.4.0) (2025-04-23) + + +### Features + +* **popup:** support 'default' border style ([8d5b0b5](https://github.com/MunifTanjim/nui.nvim/commit/8d5b0b568517935d3c84f257f272ef004d9f5a59)) +* **popup:** use 'winborder' for default border ([118a12f](https://github.com/MunifTanjim/nui.nvim/commit/118a12f6304759d95d0d003f64067d93572b3238)) +* **popup:** use same zindex for border ([a2bc1e9](https://github.com/MunifTanjim/nui.nvim/commit/a2bc1e9d0359caa5d11ad967cd1e30e8d4676226)) +* **table:** accept param 'position' for method 'get_cell' ([8794284](https://github.com/MunifTanjim/nui.nvim/commit/87942848c93668532f46ec61ccc7aff7abf7d37e)) +* **table:** expose NuiTable.Cell.range ([b81333d](https://github.com/MunifTanjim/nui.nvim/commit/b81333d12f824dbed5eb231c8a4409a290fdd848)) + + +### Bug Fixes + +* consider multi-byte characters when truncating text ([53e907f](https://github.com/MunifTanjim/nui.nvim/commit/53e907ffe5eedebdca1cd503b00aa8692068ca46)) +* **input:** cursor position patch w/ relative=cursor ([b18316d](https://github.com/MunifTanjim/nui.nvim/commit/b18316d50538bb3f40c8fd2c887490c9d29f8811)) +* **input:** cursor position patch w/ relative=editor ([a3597dc](https://github.com/MunifTanjim/nui.nvim/commit/a3597dc88b53489d3fddbddbbd13787355253bb0)) +* **input:** ignore 'keymap' for feeding default value ([fbb139c](https://github.com/MunifTanjim/nui.nvim/commit/fbb139c6f14896b434d0229099e1acd863ae6bec)) +* **input:** mounting multiple inputs together ([322978c](https://github.com/MunifTanjim/nui.nvim/commit/322978c734866996274467de084a95e4f9b5e0b1)) +* **layout:** enable nested flag for WinClosed autocmd ([fc59553](https://github.com/MunifTanjim/nui.nvim/commit/fc59553b5a8a1c13b8aa25ae62b6a47ec2b1882c)) +* **layout:** handle if child.grow results in height <= 0 ([61574ce](https://github.com/MunifTanjim/nui.nvim/commit/61574ce6e60c815b0a0c4b5655b8486ba58089a1)) +* **layout:** immediate hide/unmount after mounting ([42e4756](https://github.com/MunifTanjim/nui.nvim/commit/42e47565ecbd22306205904e21b45c169812525c)) +* **layout:** starting current position for nested boxes ([1b24de4](https://github.com/MunifTanjim/nui.nvim/commit/1b24de4778de527ef82adad6d0e819819d946387)) +* **popup:** always use current window for relative=cursor ([3dc46d7](https://github.com/MunifTanjim/nui.nvim/commit/3dc46d725f7b94bee5117c0a699b57b1902b5d65)) +* **popup:** ignore 'winborder' for complex border ([aa29efe](https://github.com/MunifTanjim/nui.nvim/commit/aa29efe58f2e5734ff49b44c3d7d0cd4b9266e9a)) +* **popup:** mimic native 'solid' border ([cbd2668](https://github.com/MunifTanjim/nui.nvim/commit/cbd2668414331c10039278f558630ed19b93e69b)) +* **tree:** update error message for duplicate node id ([8d3bce9](https://github.com/MunifTanjim/nui.nvim/commit/8d3bce9764e627b62b07424e0df77f680d47ffdb)) + +## [0.3.0](https://github.com/MunifTanjim/nui.nvim/compare/0.2.0...0.3.0) (2024-02-16) + + +### Features + +* **layout:** method 'show' mounts if not already mounted ([c1627d0](https://github.com/MunifTanjim/nui.nvim/commit/c1627d07dbd64fac9bab213e30df7ec044ac32c6)) +* **popup:** method 'show' mounts if not already mounted ([ecd77d8](https://github.com/MunifTanjim/nui.nvim/commit/ecd77d8b5d917714f4e4f7bf5b7e91184c6cecae)) +* **split:** method 'show' mounts if not already mounted ([43f7605](https://github.com/MunifTanjim/nui.nvim/commit/43f7605f864d82ab1a2642541814465c25fb76d8)) +* support decimal number in (0,1) range as size ([35da9ca](https://github.com/MunifTanjim/nui.nvim/commit/35da9ca1de0fc4dda96c2e214d93d363c145f418)) +* **tree:** allow node:expand with zero child ([0f913a3](https://github.com/MunifTanjim/nui.nvim/commit/0f913a3ae1a24c8a4487fbf111b4044cc22b1b0d)) +* **utils:** replace deprecated api to set option ([401a7c6](https://github.com/MunifTanjim/nui.nvim/commit/401a7c65bfd6433e1b0b48d2c246e2621fc44387)) + + +### Bug Fixes + +* **input:** check mounted state in mount/unmount method ([c0c8e34](https://github.com/MunifTanjim/nui.nvim/commit/c0c8e347ceac53030f5c1ece1c5a5b6a17a25b32)) +* **input:** skip cursor position patch for hidden input ([80445d0](https://github.com/MunifTanjim/nui.nvim/commit/80445d015d2b5f9af0d9e8bce63d303bc86eda8a)) +* **menu:** defer execution of `make_default_prepare_node` ([49182fa](https://github.com/MunifTanjim/nui.nvim/commit/49182fae69bd3f9be33862f106c1bb9f6bc3b4f5)) +* **menu:** set default zindex higher than popup ([abb0662](https://github.com/MunifTanjim/nui.nvim/commit/abb066278507040e4c1ddf1c53ccde3139b42ab0)) +* **popup:** make sure border buf is modifiable ([aa1b4c1](https://github.com/MunifTanjim/nui.nvim/commit/aa1b4c1e05983ff7debd2b4b2788651db099de2f)) +* **popup:** support border:set_text before layout mount ([c9b4de6](https://github.com/MunifTanjim/nui.nvim/commit/c9b4de623d19a85b353ff70d2ae9c77143abe69c)) +* **popup:** use popup winblend for border ([257dccc](https://github.com/MunifTanjim/nui.nvim/commit/257dccc43b4badc735978f0791d216f7d665b75a)) +* **split:** for relative=editor always split from current window ([af8ddf5](https://github.com/MunifTanjim/nui.nvim/commit/af8ddf5db7e8485051aacefb24d76ab24ea26a0c)) +* **split:** manual doautocmd BufWinEnter ([af7dfee](https://github.com/MunifTanjim/nui.nvim/commit/af7dfee12fbf51d12cfc6ee386fa54f7a5a573c8)) + +## [0.2.0](https://github.com/MunifTanjim/nui.nvim/compare/0.1.0...0.2.0) (2023-06-18) + + +### Features + +* **input:** call on_close inside :unmount ([60e91dd](https://github.com/MunifTanjim/nui.nvim/commit/60e91dd3d3b19dfe1ca5833600dd94381657f035)) +* **input:** update types ([c3c2957](https://github.com/MunifTanjim/nui.nvim/commit/c3c2957f2d5e2d5ebeb1be421cb0a2f4b50461ff)) +* **layout:** support anchor for float layout ([f284153](https://github.com/MunifTanjim/nui.nvim/commit/f2841533540eb60d6878964a3cd3f8196e1f200c)) +* **layout:** update types ([62b3203](https://github.com/MunifTanjim/nui.nvim/commit/62b320361fe0c93697a600fd4ca9ede295bd3c81)) +* **line:** update types ([1a8d824](https://github.com/MunifTanjim/nui.nvim/commit/1a8d8240b458a6b82751702bfb217f00eaf305b6)) +* **menu:** update types ([0a97a88](https://github.com/MunifTanjim/nui.nvim/commit/0a97a88bf28c8545550bcbcdd28a03b428647dbc)) +* **popup:** add method border:set_style ([9d98e9b](https://github.com/MunifTanjim/nui.nvim/commit/9d98e9bac8cf681a608ef20c0a2205354a77c419)) +* **popup:** create border buffer on initialization ([643e9af](https://github.com/MunifTanjim/nui.nvim/commit/643e9afb9411f5ebd95efb43437692e74238a4a3)) +* **popup:** support `(text, hl_group)[]` for border text ([062e366](https://github.com/MunifTanjim/nui.nvim/commit/062e366afcdf2bc1e9d28313a1df4ff14f05cb4e)) +* **popup:** support anchor in :update_layout ([52c9115](https://github.com/MunifTanjim/nui.nvim/commit/52c9115b10b22a2b8416bdab3072662d67d91ed6)) +* **popup:** support nui.line for border text ([1b8fa8b](https://github.com/MunifTanjim/nui.nvim/commit/1b8fa8b2adaf3583a05f53b505093017d23cd62f)) +* **popup:** update types ([f6b6923](https://github.com/MunifTanjim/nui.nvim/commit/f6b6923883491aeb7ce8dbae3b7b3767e8376aa8)) +* **split:** update types ([f4469cb](https://github.com/MunifTanjim/nui.nvim/commit/f4469cb716ba4430e22ad7f0c71ef4da8baf1f34)) +* support byte range for _.render_lines util ([993d550](https://github.com/MunifTanjim/nui.nvim/commit/993d5500f1c09710feae07a7887cf9a36cd7e02d)) +* **table:** add nui.table block ([bfd3806](https://github.com/MunifTanjim/nui.nvim/commit/bfd3806904c29babfa61705a37e8b32ab687d2d2)) +* **table:** support linenr_start for render method ([457a5cf](https://github.com/MunifTanjim/nui.nvim/commit/457a5cfe43a18d21337045b6026818a2898144d1)) +* **table:** update types ([64bdc57](https://github.com/MunifTanjim/nui.nvim/commit/64bdc579873fa5bd303f6951ead2b419493c88e8)) +* **text:** update types ([9f7666d](https://github.com/MunifTanjim/nui.nvim/commit/9f7666d89f9b4abf76d7db25a0511833dc72e7c1)) +* **tree:** always track linenr in :render ([f008972](https://github.com/MunifTanjim/nui.nvim/commit/f008972ac7d24f7188521a7f8d158aac2fb0b07e)) +* **tree:** update types ([d3cc976](https://github.com/MunifTanjim/nui.nvim/commit/d3cc9762581afa19e86353238423f521a61aeea4)) +* **utils:** support string line in _.render_lines ([51764d2](https://github.com/MunifTanjim/nui.nvim/commit/51764d2c2235ad944ef4086d5f3954305728e5bd)) +* **utils:** update types ([df321ba](https://github.com/MunifTanjim/nui.nvim/commit/df321ba052f37715ea5936a52d862a12efd836c6)) + + +### Bug Fixes + +* **input:** unmount race condition ([e319f25](https://github.com/MunifTanjim/nui.nvim/commit/e319f2554d14a521f4271576ebff2685105d7628)) +* **layout:** even more patch for neovim/neovim[#18925](https://github.com/MunifTanjim/nui.nvim/issues/18925) ([de66444](https://github.com/MunifTanjim/nui.nvim/commit/de6644476702ba39344f1b28900b74381bc8c4c9)) +* **layout:** more robust workaround for neovim/neovim[#18925](https://github.com/MunifTanjim/nui.nvim/issues/18925) ([9230eb0](https://github.com/MunifTanjim/nui.nvim/commit/9230eb01fb34f81b7b31ac77dcfdf356a71e487e)) +* **popup:** border empty char handling ([bd2fefb](https://github.com/MunifTanjim/nui.nvim/commit/bd2fefb2efac70231fee497137295333dd4ada30)) +* **popup:** ensure valid border.bufnr on mount ([6867305](https://github.com/MunifTanjim/nui.nvim/commit/6867305a508e374b1f4ec84ba0efa59351c6f7e6)) +* **table:** update types ([d5a82aa](https://github.com/MunifTanjim/nui.nvim/commit/d5a82aae64426a805e19d8ef5a379292f9dc55d3)) +* **tree:** do not wipe linenr tracking in :set_nodes ([da3b5eb](https://github.com/MunifTanjim/nui.nvim/commit/da3b5eb197391f3e1fac6f79c75e123ae9590be3)) + + +### Performance Improvements + +* **tree:** optimize hot paths ([19de4d5](https://github.com/MunifTanjim/nui.nvim/commit/19de4d5299be40a9aada6af940daeef20a59929e)) + +## 0.1.0 (2023-05-27) + + +### ⚠ BREAKING CHANGES + +* **line:** change parameter order for methods. +* **text:** change parameter order for methods. + +### Features + +* accept multiple keys for keymap ([4998347](https://github.com/MunifTanjim/nui.nvim/commit/4998347f02fde115ad0c023b90be9c5654834635)) +* add internal utils._.calculate_gap_width ([90d7285](https://github.com/MunifTanjim/nui.nvim/commit/90d7285c182b396c21ba69684068fd7d5d6eba79)) +* add util to clear namespace for buffer ([6c63bf5](https://github.com/MunifTanjim/nui.nvim/commit/6c63bf5fd51076455d1b04f5c09d9841ab081263)) +* **bar:** add lower-level `core.add_highlight` function ([20385a6](https://github.com/MunifTanjim/nui.nvim/commit/20385a698e8a5dd98ee7e63f16b700a10b921098)) +* **bar:** add some lower-level `core.add_*` functions ([5d1ca66](https://github.com/MunifTanjim/nui.nvim/commit/5d1ca66829d8fac9965cd18fcc2cd9aa49ba1ea5)) +* **bar:** initial implementation ([35758e9](https://github.com/MunifTanjim/nui.nvim/commit/35758e946a64376e0e9625a27469410b3d1f9223)) +* **bar:** remove module ([0dc148c](https://github.com/MunifTanjim/nui.nvim/commit/0dc148c6ec06577fcf06cbab3b7dac96d48ba6be)) +* **input:** add component Input ([7307c94](https://github.com/MunifTanjim/nui.nvim/commit/7307c94a7a0954f1c717e094c39772f28bb9d13f)) +* **input:** move internal default_value and prompt prop ([595a2ea](https://github.com/MunifTanjim/nui.nvim/commit/595a2ea90f7d31bc23386ba67939117d45771035)) +* **input:** support nui.text for options.prompt ([c380563](https://github.com/MunifTanjim/nui.nvim/commit/c38056355f5a72c7a1a005f7125afd62ed7b2083)) +* **layout:** add method layout:update ([3f611a6](https://github.com/MunifTanjim/nui.nvim/commit/3f611a674254370d6d73f9a35a3eedbc8d07ee3b)) +* **layout:** add some util for size and position ([cc3b970](https://github.com/MunifTanjim/nui.nvim/commit/cc3b970c1537de9562f82e5fa6aa126a629242d1)) +* **layout:** feature guard lua autocmd api usage ([3dc6b89](https://github.com/MunifTanjim/nui.nvim/commit/3dc6b89bda8f1616a2638727eeb75a4a770e1c21)) +* **layout:** initial implementation ([716c3f9](https://github.com/MunifTanjim/nui.nvim/commit/716c3f9d857b62a086b4f9123d0d34d1b7733922)) +* **layout:** introduce layout type ([a5fd005](https://github.com/MunifTanjim/nui.nvim/commit/a5fd005263d238d2fbd6ee335e06139645f11fa9)) +* **layout:** introduce split layout ([042cceb](https://github.com/MunifTanjim/nui.nvim/commit/042cceb497cc4cfa3ae735a5e7bc01b4b6f19ef1)) +* **layout:** make window transparent ([2d33512](https://github.com/MunifTanjim/nui.nvim/commit/2d33512836cb603d58cb538e716a02c44547c6ab)) +* **layout:** re-use windows for unchanged split layout box ([71ddaaf](https://github.com/MunifTanjim/nui.nvim/commit/71ddaafadfd8b3c21f809ea26fb4fe2a110bbf54)) +* **layout:** support :show and :hide for float layout ([f572782](https://github.com/MunifTanjim/nui.nvim/commit/f572782bd8cc6b6b8671566b09146c4c09f20ae2)) +* **layout:** support container component ([5bc7376](https://github.com/MunifTanjim/nui.nvim/commit/5bc737602e231111c44458c84e3eca05393f355f)) +* **layout:** support o.grow factor for layout.box ([796dc82](https://github.com/MunifTanjim/nui.nvim/commit/796dc8293be59acc0c0d53b99e7dca7c32db4413)) +* **layout:** support o.grow for layout.box ([2c6bac9](https://github.com/MunifTanjim/nui.nvim/commit/2c6bac942e3af35eb42165c923c0d3dc60a46430)) +* **layout:** throw for empty box at init ([6e872b3](https://github.com/MunifTanjim/nui.nvim/commit/6e872b3bf28aaf52fd1b831ea0690aea205d6464)) +* **layout:** tweak component wire up ([eed888e](https://github.com/MunifTanjim/nui.nvim/commit/eed888e47fa91980ce295dd347b213a102200b5a)) +* **layout:** use backported autocmd methods ([4e21085](https://github.com/MunifTanjim/nui.nvim/commit/4e21085cd1b6be12da7ff0e7168e9100c51c363a)) +* **layout:** wire up float layout components ([3aa617d](https://github.com/MunifTanjim/nui.nvim/commit/3aa617d9054e052cde68cd3141db4396af93a9cb)) +* **line:** accept initial nui.text objects ([2f44cc9](https://github.com/MunifTanjim/nui.nvim/commit/2f44cc941cf1a129b6b6069a7489672b24cf0015)) +* **line:** accept NuiText object as param for line:append() method ([34fd4bf](https://github.com/MunifTanjim/nui.nvim/commit/34fd4bfde84ff4b735a98dfb3508280d39e520f6)) +* **line:** add method :width ([80122e5](https://github.com/MunifTanjim/nui.nvim/commit/80122e542fcebc361c4ca585be40a071c6e360be)) +* **line:** add nui.line block ([1333fd0](https://github.com/MunifTanjim/nui.nvim/commit/1333fd07c57310d1421dcb337a21fbddb20d3c84)) +* **line:** make ns_id required, update method signature ([5695bde](https://github.com/MunifTanjim/nui.nvim/commit/5695bde7ac9b8bcbb1df237a05169bad99458090)) +* **line:** support nui.line in method :append ([401a69f](https://github.com/MunifTanjim/nui.nvim/commit/401a69f27ede2d5a9a725f4f45758708d3b72d09)) +* make components extendable ([c75976e](https://github.com/MunifTanjim/nui.nvim/commit/c75976e823085218aeef4f5312a8c88e7d333358)) +* **menu:** add component Menu ([dca0630](https://github.com/MunifTanjim/nui.nvim/commit/dca0630b0d7ab5dfcabaa70284c94c2e133b8200)) +* **menu:** expose .tree ([d12a697](https://github.com/MunifTanjim/nui.nvim/commit/d12a6977846b2fa978bff89b439e509320854e10)) +* **menu:** improve menu separator implementation ([0e05425](https://github.com/MunifTanjim/nui.nvim/commit/0e0542505369861fc22f6bff3ad2976bdef6d8f3)) +* **menu:** move internal props ([0373a94](https://github.com/MunifTanjim/nui.nvim/commit/0373a94bc79725726aadcc1e0e26de159e3152e1)) +* **menu:** pass self to on_change callback ([4438c5e](https://github.com/MunifTanjim/nui.nvim/commit/4438c5e53f8a5c834569309a5d3e276e6bf1abe9)) +* **menu:** rename method menu:init to menu:new ([2825c3d](https://github.com/MunifTanjim/nui.nvim/commit/2825c3d60438bbed9c04f18f030ab3505249e8a7)) +* **menu:** simplify automatic width calculation ([54cbaf5](https://github.com/MunifTanjim/nui.nvim/commit/54cbaf5d227cfedbb939d16b405116e4cea6e3fb)) +* **menu:** support arbritary props for Menu.item ([77cefa6](https://github.com/MunifTanjim/nui.nvim/commit/77cefa67df7f282db20aa9afb5925aff79455dc2)) +* **menu:** support nui.line for Menu.item ([51cbd0c](https://github.com/MunifTanjim/nui.nvim/commit/51cbd0ccc9410e317a947eea1e99966226a5f8b5)) +* **menu:** support nui.text as separator char ([3029554](https://github.com/MunifTanjim/nui.nvim/commit/30295541c1bda2ab171ab7b684f790be3fbb60a7)) +* **menu:** support nui.text for Menu.item ([13e557d](https://github.com/MunifTanjim/nui.nvim/commit/13e557d045b62efc6da87bdcc77503b5adf1db21)) +* **menu:** support options.on_change ([db06fed](https://github.com/MunifTanjim/nui.nvim/commit/db06feddf324d4c6c8763fe9fca43b9140de84a9)) +* **object:** add helper functions ([9531977](https://github.com/MunifTanjim/nui.nvim/commit/95319774b31558479c71e9b190870aae7bd8e49f)) +* **object:** initial implementation ([194837f](https://github.com/MunifTanjim/nui.nvim/commit/194837ffc4a9c77dfcc18809c7e43a02b4d11e03)) +* **popup:** add method .border:set_highlight ([acb72b1](https://github.com/MunifTanjim/nui.nvim/commit/acb72b150c7fdf01331b08460ec07fe7a81029b6)) +* **popup:** add method popup:set_layout(config) ([006711f](https://github.com/MunifTanjim/nui.nvim/commit/006711f3ab4e8626f47b2b903759a9ac0e232fbd)) +* **popup:** add method popup:set_position(position, relative) ([7ea1a6b](https://github.com/MunifTanjim/nui.nvim/commit/7ea1a6b910b1b33fceb33068df8f31e14ccceb1e)) +* **popup:** add method popup:set_size(size) ([07b6e9a](https://github.com/MunifTanjim/nui.nvim/commit/07b6e9a90b58af8ca9c09f3695aa14603c947b61)) +* **popup:** add method popup:unmap ([46bbf33](https://github.com/MunifTanjim/nui.nvim/commit/46bbf336e9068c73c4e810cb93c7a1f8197ebbc3)) +* **popup:** add method popup.border:set_text(...) ([b82a5d3](https://github.com/MunifTanjim/nui.nvim/commit/b82a5d3bda43b5cd9b9dca39b782e86d2735933f)) +* **popup:** add methods popup:hide() and popup:show() ([b3c706d](https://github.com/MunifTanjim/nui.nvim/commit/b3c706d4c30bd5cd8b05d10a2c8bf8fcd1f5cf7a)) +* **popup:** add methods popup:on(...) and popup:off(...) ([0eb57a2](https://github.com/MunifTanjim/nui.nvim/commit/0eb57a2bdd565dfad09e4bda4293ef11c17d741f)) +* **popup:** add option 'focusable' ([8339965](https://github.com/MunifTanjim/nui.nvim/commit/8339965e991ad54e6606ea22213996521701e293)) +* **popup:** add option padding ([596cd77](https://github.com/MunifTanjim/nui.nvim/commit/596cd77a875eef1c6629e12074139e2ca0033e38)) +* **popup:** add options 'buf_options' and 'win_options' ([163d99a](https://github.com/MunifTanjim/nui.nvim/commit/163d99a3fb9fe9a630c96bbf6159973885c3caf3)) +* **popup:** add options.ns_id ([6f165aa](https://github.com/MunifTanjim/nui.nvim/commit/6f165aad4d2ac5a5a279a695ed247ec954cdcc4b)) +* **popup:** add type annotation for bufnr,winid,ns_id ([c2d1f73](https://github.com/MunifTanjim/nui.nvim/commit/c2d1f73eb0ab02e90e8316a6e70158d2eec72153)) +* **popup:** add type annotation for win_config ([f8ccc5c](https://github.com/MunifTanjim/nui.nvim/commit/f8ccc5cf8e8aec7ff34572b80e7b5d92b2d07556)) +* **popup:** allow layout refresh when container size changes ([ee8d315](https://github.com/MunifTanjim/nui.nvim/commit/ee8d315456691fc1d05dcec465d95f8aec902541)) +* **popup:** change behavior of padding ([9f29df4](https://github.com/MunifTanjim/nui.nvim/commit/9f29df4153da1483ad784a473f5bbcf02fcbbe3b)) +* **popup:** clear namespace object on unmount ([58e06b0](https://github.com/MunifTanjim/nui.nvim/commit/58e06b0175cf22672d96a522343ec6ce017ba54c)) +* **popup:** create buffer on initialization ([6b1deda](https://github.com/MunifTanjim/nui.nvim/commit/6b1deda411b96f0d694dead7cc12ab9db1dd9b65)) +* **popup:** default border.text hl to FloatTitle ([4eaec2a](https://github.com/MunifTanjim/nui.nvim/commit/4eaec2ac66af2ca6ddddd3f665ad0909b90ae36a)) +* **popup:** feature guard lua autocmd api usage ([6028584](https://github.com/MunifTanjim/nui.nvim/commit/60285847a4df99c2ab0ece3020eb930c81b09c41)) +* **popup:** improve border highlight implementation ([2f58c40](https://github.com/MunifTanjim/nui.nvim/commit/2f58c406eb9cb5cedf5d82f9ce7c4f6efc418550)) +* **popup:** improve cleanup ([220d4a4](https://github.com/MunifTanjim/nui.nvim/commit/220d4a4bbaa8f7dc80c0aa37e8377c7c150c6384)) +* **popup:** merge internal position_meta into position ([bc2fc9c](https://github.com/MunifTanjim/nui.nvim/commit/bc2fc9c1b7b7241c4a0f34597f8ddc11c5f90844)) +* **popup:** move internal buf_options and win_options ([3148908](https://github.com/MunifTanjim/nui.nvim/commit/31489084b7d3363c9a6f8f4d435374a25f4f636b)) +* **popup:** move internal loading and mounted state ([4dc3214](https://github.com/MunifTanjim/nui.nvim/commit/4dc321448faaad171f0aab07d092a0e626fb1cbb)) +* **popup:** move internal position state ([d229bbb](https://github.com/MunifTanjim/nui.nvim/commit/d229bbb4846bd4268ca1f6f67543294237ee0029)) +* **popup:** move internal position_meta state ([cbbbe90](https://github.com/MunifTanjim/nui.nvim/commit/cbbbe90213091a462e71a1f102e0927ac11bac8a)) +* **popup:** move internal size prop ([0a2fced](https://github.com/MunifTanjim/nui.nvim/commit/0a2fcedb97673ba478e26a0f59edbca950d15d31)) +* **popup:** move internal win_enter prop ([f7736c9](https://github.com/MunifTanjim/nui.nvim/commit/f7736c90db395adc276519bd871bcec088bed908)) +* **popup:** remove automatic cleanup ([06b48cf](https://github.com/MunifTanjim/nui.nvim/commit/06b48cf645971e1e1d598a083b98733890a82302)) +* **popup:** remove method popup:on(...) ([c24b131](https://github.com/MunifTanjim/nui.nvim/commit/c24b13195c3e8c7c42d64736d96ded0ce7cb13ed)) +* **popup:** rename method :set_layout to :update_layout ([90f59b0](https://github.com/MunifTanjim/nui.nvim/commit/90f59b035565e549e467fc414f1178d30c074ce9)) +* **popup:** rename window to popup ([7097509](https://github.com/MunifTanjim/nui.nvim/commit/7097509b4b0fd21d870cea19c51389043c408449)) +* **popup:** rework border internals ([8a776a2](https://github.com/MunifTanjim/nui.nvim/commit/8a776a2033220b16fa032b9d3590c75a178435ec)) +* **popup:** simplify border highlight mechanism ([0ec30d9](https://github.com/MunifTanjim/nui.nvim/commit/0ec30d912c473139832d5c0bfd8ccfa675a48974)) +* **popup:** simplify border.text ([674305a](https://github.com/MunifTanjim/nui.nvim/commit/674305a0cb1df2b2c0f51ade7081235755e93643)) +* **popup:** support nui.text for simple border ([5f5a2e5](https://github.com/MunifTanjim/nui.nvim/commit/5f5a2e5284a08f930098383a6d0a00b861fc58d2)) +* **popup:** support NuiText as border.text ([1248c67](https://github.com/MunifTanjim/nui.nvim/commit/1248c674a4a632ea7d45a76b1255df997379e116)) +* **popup:** support option 'anchor' ([a86c733](https://github.com/MunifTanjim/nui.nvim/commit/a86c733e7d30596d80927b5eaf6869aa9a3d30af)) +* **popup:** support set_size when unmounted ([fa86f85](https://github.com/MunifTanjim/nui.nvim/commit/fa86f8539373e1b76b93f95dfa36a5793ed4fe35)) +* **popup:** support unmanaged buffer ([335415a](https://github.com/MunifTanjim/nui.nvim/commit/335415af52ea23d07433aa1f72f7c0d56c219316)) +* **popup:** use backported autocmd methods ([372369d](https://github.com/MunifTanjim/nui.nvim/commit/372369dea4c059f831e540e34b9a49bf183c245b)) +* **split:** add component Split ([bcb7382](https://github.com/MunifTanjim/nui.nvim/commit/bcb73828d54169f5ae9d141bca05fecb6aec5ec5)) +* **split:** add method :update_layout ([32f44a6](https://github.com/MunifTanjim/nui.nvim/commit/32f44a610691ff2e22e60a8040801e047112806f)) +* **split:** add method split:unmap ([e0444e0](https://github.com/MunifTanjim/nui.nvim/commit/e0444e020fd3ed336e2b6ba998ff7903ddb68ddd)) +* **split:** create buffer on initialization ([0e36b78](https://github.com/MunifTanjim/nui.nvim/commit/0e36b78b836200ef83ef723c6a0ebb753ac8a11e)) +* **split:** feature guard lua autocmd api usage ([d14daab](https://github.com/MunifTanjim/nui.nvim/commit/d14daab6710d1ebd7ec868f15bddde2bbd2c186f)) +* **split:** improve cleanup ([36b0649](https://github.com/MunifTanjim/nui.nvim/commit/36b0649d7df3d1899d5aed8b27d22159cf058a2e)) +* **split:** move internal buf_options and win_options ([6c9a3ee](https://github.com/MunifTanjim/nui.nvim/commit/6c9a3ee9fdb2c7a48cf13d9c82ee56822c5bfdb8)) +* **split:** move internal loading and mounted state ([653199c](https://github.com/MunifTanjim/nui.nvim/commit/653199c635ad56c1e313e0890c6a72da8d3ee3bd)) +* **split:** move internal props ([25e51eb](https://github.com/MunifTanjim/nui.nvim/commit/25e51eba14cdf2d445d0c0efde78d808741483cb)) +* **split:** set buffer after .winid is set ([35091ca](https://github.com/MunifTanjim/nui.nvim/commit/35091ca0f8c766ea3542508f3ab9217a746cc5a0)) +* **split:** store id internally ([96ef1cb](https://github.com/MunifTanjim/nui.nvim/commit/96ef1cb4e3c830dc79e18acac209ea5f7eb78829)) +* **split:** support o.enter ([b75e2e6](https://github.com/MunifTanjim/nui.nvim/commit/b75e2e6d2a86a1105a756b894385d5fe836c3821)) +* **split:** support o.ns_id ([28cafab](https://github.com/MunifTanjim/nui.nvim/commit/28cafab82f5d5ffc4321b9f82bae7b38951434d1)) +* **split:** support o.relative.winid ([82851af](https://github.com/MunifTanjim/nui.nvim/commit/82851af021d651bb6bf6da0991c79f4c529ff37e)) +* **split:** tweak split size handling for new window ([b681ab2](https://github.com/MunifTanjim/nui.nvim/commit/b681ab2a8a8750b37dcece4e87105a5671c39745)) +* **split:** use backported autocmd methods ([6bd1d8a](https://github.com/MunifTanjim/nui.nvim/commit/6bd1d8af326c654a8b9d122c32a62fed5e55fe89)) +* **text:** add method :new ([4ad7811](https://github.com/MunifTanjim/nui.nvim/commit/4ad781109a44bc00fa7d1208576f4086add27ad3)) +* **text:** add method text:set(content, highlight?) ([eaf4844](https://github.com/MunifTanjim/nui.nvim/commit/eaf4844e9e84994705acc0347673a6b15e7c030f)) +* **text:** add nui.text block ([a6df800](https://github.com/MunifTanjim/nui.nvim/commit/a6df800df514c2f0a9dd88d8db267ec5b8c8d68d)) +* **text:** change highlight table key group->hl_group ([a8aaca1](https://github.com/MunifTanjim/nui.nvim/commit/a8aaca1578dff3504e78538b477910de344af0f8)) +* **text:** make ns_id required, update method signature ([b63d199](https://github.com/MunifTanjim/nui.nvim/commit/b63d199ddfdc89457ae401ed0a5659fa1055c37c)) +* **text:** preserve own extmark id ([26622d1](https://github.com/MunifTanjim/nui.nvim/commit/26622d147762f2212bf30e0792df1d0164a73cd9)) +* **text:** remove method :new ([18a0390](https://github.com/MunifTanjim/nui.nvim/commit/18a0390d5caca31b310af41787e5ddd407c68783)) +* **text:** return self from method text:set ([c5971ed](https://github.com/MunifTanjim/nui.nvim/commit/c5971ed28773cf9b6ce4ed8bfbc20266aa05b2a1)) +* **text:** support cloning and use extmarks ([281d453](https://github.com/MunifTanjim/nui.nvim/commit/281d4535d046b1dd09d45bca8b0739bb16461b75)) +* **text:** support extmark override when cloning ([878dfaf](https://github.com/MunifTanjim/nui.nvim/commit/878dfaf85fa19ff6811f19f1dad0bbfda77d5bbf)) +* **tree:** add method node:get_child_ids ([bc05620](https://github.com/MunifTanjim/nui.nvim/commit/bc056204ff06b205dd3804474fc155180e704d47)) +* **tree:** add method tree:get_nodes ([f85aedc](https://github.com/MunifTanjim/nui.nvim/commit/f85aedc1378acb375e4da0ec2220c1df0929e843)) +* **tree:** add method tree:set_nodes(nodes, parent_id?) ([b25fab5](https://github.com/MunifTanjim/nui.nvim/commit/b25fab59d997cd9793bf7f42a1e41b9b7684d987)) +* **tree:** add nui.tree block ([3bdfa78](https://github.com/MunifTanjim/nui.nvim/commit/3bdfa780fdba053358b85fd1f276052ae1455b61)) +* **tree:** add o.bufnr and deprecate o.winid ([ce4869f](https://github.com/MunifTanjim/nui.nvim/commit/ce4869f97e4f3d4f9eb64d021fff775684ae8859)) +* **tree:** clear namespace before render ([1f66cc7](https://github.com/MunifTanjim/nui.nvim/commit/1f66cc794bb6cad23f97b71e099cc32dd63c0614)) +* **tree:** make node:has_children method work before init ([96f600b](https://github.com/MunifTanjim/nui.nvim/commit/96f600b58f128bde4a78a56c0a64d55664a43955)) +* **tree:** move internal buf_options and win_options ([3a7c0c4](https://github.com/MunifTanjim/nui.nvim/commit/3a7c0c48b279ee0495c5ec61ca312fddd052195c)) +* **tree:** move internal get_node_id and prepare_node functions ([1b9e046](https://github.com/MunifTanjim/nui.nvim/commit/1b9e04685097b93a4f8d01002adaaa5dcfb327af)) +* **tree:** pass parent_node to prepare_node function ([559d33d](https://github.com/MunifTanjim/nui.nvim/commit/559d33dcace8016603129e63e5cb605aaa059c10)) +* **tree:** rename options.ns to options.ns_id ([5db3901](https://github.com/MunifTanjim/nui.nvim/commit/5db390110bf9944b678c84cd7bcd2a28af712481)) +* **tree:** return end linenr from tree:get_node method ([8ae5e31](https://github.com/MunifTanjim/nui.nvim/commit/8ae5e3106a0fa17144a0a086ccfce1e73a73f19f)) +* **tree:** return linenr from tree:get_node method ([3b746d7](https://github.com/MunifTanjim/nui.nvim/commit/3b746d7b6f16818a970d1c4810261d18b958a956)) +* **tree:** support linenr for method tree:get_node ([7fee7c6](https://github.com/MunifTanjim/nui.nvim/commit/7fee7c6c176e83806a144f7e0a8ac6251920a8a7)) +* **tree:** support linenr_start for method :render ([afb9e5b](https://github.com/MunifTanjim/nui.nvim/commit/afb9e5b4512e17d879fb069e77de4141472d0fb9)) +* **tree:** support multiline node ([4926ee9](https://github.com/MunifTanjim/nui.nvim/commit/4926ee9ba8fac49ad23cd695f1f5c0952c52dd4a)) +* **tree:** support nil return for o.prepare_node ([5a79b1b](https://github.com/MunifTanjim/nui.nvim/commit/5a79b1b3b8231cfa33290d8d3c56d36b6496499e)) +* use api-autocmd if available ([3f05d74](https://github.com/MunifTanjim/nui.nvim/commit/3f05d742b273ed4b48db4e5ab99c09b8b05cd537)) +* use native keymap callback if supported ([ad2c05c](https://github.com/MunifTanjim/nui.nvim/commit/ad2c05c983dda8d423d952fce55eb3cf3966a1ea)) +* **utils:** add autocmd ([1f51d5a](https://github.com/MunifTanjim/nui.nvim/commit/1f51d5a6735153ea5e3f9a1f68a12c9f1bce5681)) +* **utils:** add buf_storage ([6300e3b](https://github.com/MunifTanjim/nui.nvim/commit/6300e3bdcc2fc38e361a48b76247c4524a0fb27a)) +* **utils:** backport autocmd to nvim < 0.7.x ([587a49f](https://github.com/MunifTanjim/nui.nvim/commit/587a49f90fb036a6d94904851dafdd53d1327fe0)) +* **utils:** move keymap to utils ([56c2230](https://github.com/MunifTanjim/nui.nvim/commit/56c223041ad342b2a8d28d2bb1dbd88dc0d7839a)) +* **utils:** update autocmd.event ([6f9153c](https://github.com/MunifTanjim/nui.nvim/commit/6f9153cc8462a3bb0a8a883694befa6554d31403)) +* **window:** add method window:destroy() ([c51856d](https://github.com/MunifTanjim/nui.nvim/commit/c51856da53df1327a406e7983d5fd748486fc339)) +* **window:** add method window:map(...) ([f69ee04](https://github.com/MunifTanjim/nui.nvim/commit/f69ee0498ecc57268d23a4b51516002404c3def1)) +* **window:** add method window:on(event_name, handler) ([f36b496](https://github.com/MunifTanjim/nui.nvim/commit/f36b496b52aec95dc9e122830b0d2ff64eeb21f5)) +* **window:** add method window:render() ([d1a047d](https://github.com/MunifTanjim/nui.nvim/commit/d1a047d22d0794943a08a2de598ac57c61fc4978)) +* **window:** add option highlight ([c4bbe61](https://github.com/MunifTanjim/nui.nvim/commit/c4bbe6139f012aca971ea26721dc4c1942b71286)) +* **window:** change default border to none ([8db2faa](https://github.com/MunifTanjim/nui.nvim/commit/8db2faa3dfa9b196fe2fd11311c534b37db2428a)) +* **window:** enhanced border support ([a17070c](https://github.com/MunifTanjim/nui.nvim/commit/a17070c6b55ba36db82edef0c3af28ac0929e649)) +* **window:** initial implementation ([19e4bb6](https://github.com/MunifTanjim/nui.nvim/commit/19e4bb669d2fdd168a6d0a3edebaca150f93678f)) +* **window:** rename 'destroy' to 'unmount' ([c409518](https://github.com/MunifTanjim/nui.nvim/commit/c409518e6640e50dc754b199cd37246f60a3fdbc)) +* **window:** rename 'render' to 'mount' ([66190d2](https://github.com/MunifTanjim/nui.nvim/commit/66190d237fc61db2135c0c19eb9091091d9090dc)) +* **window:** simplify option relative ([5584892](https://github.com/MunifTanjim/nui.nvim/commit/55848921914083d57e178448f3714f3ba4d9fc75)) + + +### Bug Fixes + +* **bar:** rename tabnr to tabid in context ([f220495](https://github.com/MunifTanjim/nui.nvim/commit/f2204952ba670372de507bdd446c6a14f821ac73)) +* **bar:** type for generator ([2a6533f](https://github.com/MunifTanjim/nui.nvim/commit/2a6533fb798efad7dd783311315bab8dc5eb381b)) +* **input:** escape multi-byte chars in default value ([698e758](https://github.com/MunifTanjim/nui.nvim/commit/698e75814cd7c56b0dd8af4936bcef2d13807f3c)) +* **input:** stopinsert on close ([971cca4](https://github.com/MunifTanjim/nui.nvim/commit/971cca41914aaefc9569b8a1904cf8c25cec5aa8)) +* **input:** try to keep stable cursor position on parent window ([6f803e8](https://github.com/MunifTanjim/nui.nvim/commit/6f803e88093573f73d4ee6c0dfe0575df3f97a9f)) +* **layout:** apply size for first child box in split layout ([dde3f89](https://github.com/MunifTanjim/nui.nvim/commit/dde3f89b74b726eacfa6bea82c895af265573fe4)) +* **layout:** float position calculation for child with complex border ([257da38](https://github.com/MunifTanjim/nui.nvim/commit/257da38029d3859ed111804f9d4e95b0fa993a31)) +* **layout:** focus on relative.win for split ':update' ([6e8f9a0](https://github.com/MunifTanjim/nui.nvim/commit/6e8f9a0f280fada3f8ce694a42f8376a31e9776e)) +* **layout:** o.relative for split layout ([bf5900f](https://github.com/MunifTanjim/nui.nvim/commit/bf5900f1b60bf6499755ac92315181a24a87a577)) +* **layout:** preserve split win_options 'winfixheight' and 'winfixwidth' ([ecd9def](https://github.com/MunifTanjim/nui.nvim/commit/ecd9def93891b9260b15b5fcef542eaabf4145c9)) +* **layout:** process float layout box change ([51721a4](https://github.com/MunifTanjim/nui.nvim/commit/51721a409794bf2e432e99acc21b635102fedcea)) +* **layout:** process split layout box change ([b12db53](https://github.com/MunifTanjim/nui.nvim/commit/b12db5321c194c10eb34e610fb76ce2c058853fc)) +* **layout:** typo in update_layout_config util ([d5d3d6c](https://github.com/MunifTanjim/nui.nvim/commit/d5d3d6ce542b3921f8a8da213ef7b0841f6a4adc)) +* luacheck lint warnings ([e7dd31c](https://github.com/MunifTanjim/nui.nvim/commit/e7dd31c4135389e460a686d48d10ed532ca5234e)) +* **menu:** error with fallback separator char ([76fc8ed](https://github.com/MunifTanjim/nui.nvim/commit/76fc8edf771adff74aba513bd0f62e984996ef88)) +* **popup:** add border.style default value ([a07b754](https://github.com/MunifTanjim/nui.nvim/commit/a07b754552008012f2d7d3602b7a233a29d92c66)) +* **popup:** border highlight for type:complex ([151d593](https://github.com/MunifTanjim/nui.nvim/commit/151d593b28911d61b10e1a8ba14f9e4c755141aa)) +* **popup:** border padding with style=shadow ([37e0511](https://github.com/MunifTanjim/nui.nvim/commit/37e0511f189cd19eabd0e71841f10c3a39bbb62d)) +* **popup:** check bufnr is valid before clearing namespace ([62facd3](https://github.com/MunifTanjim/nui.nvim/commit/62facd37e0dd8196212399a897374f689886f500)) +* **popup:** check if border bufnr is valid before clearing namespace ([b99e6cb](https://github.com/MunifTanjim/nui.nvim/commit/b99e6cb13dc51768abc1c4c8585045a0c0459ef1)) +* **popup:** do better mount/unmount handling ([7622fcf](https://github.com/MunifTanjim/nui.nvim/commit/7622fcf3dc4cffffc666d9f1f4e646168f640a2a)) +* **popup:** do buf_storage.cleanup after buffer wipeout ([644e595](https://github.com/MunifTanjim/nui.nvim/commit/644e595a3862ca45de71ff14fa465019ecfc17ad)) +* **popup:** do BufWinEnter autocmd after self.winid is set ([4c77e3a](https://github.com/MunifTanjim/nui.nvim/commit/4c77e3a064b7b0fdfb5f2729500a81b431ff86f8)) +* **popup:** do not reset win_options on update_layout call ([1f43b13](https://github.com/MunifTanjim/nui.nvim/commit/1f43b13d133eb4b4f53a4485379d9afa58808389)) +* **popup:** handle border padding without text ([1f9aebc](https://github.com/MunifTanjim/nui.nvim/commit/1f9aebcaca1f43c311ba16a2aad9170d597640a8)) +* **popup:** handle border:set_text properly ([e78c822](https://github.com/MunifTanjim/nui.nvim/commit/e78c822378c102978792465dbf93b8ffb27a4cc8)) +* **popup:** handle map as border.style ([26a0eea](https://github.com/MunifTanjim/nui.nvim/commit/26a0eeaca8890b74d53a91f84520abea94fcf8ee)) +* **popup:** handle various mix of border and padding ([49a155f](https://github.com/MunifTanjim/nui.nvim/commit/49a155f3bbacb90a8b2df320f5b127b568083cd6)) +* **popup:** highlight for simple border ([3ff3d26](https://github.com/MunifTanjim/nui.nvim/commit/3ff3d2628f3c14633792f702b3cfea377addcf47)) +* **popup:** ignore WinClosed from other popup ([a501202](https://github.com/MunifTanjim/nui.nvim/commit/a5012020a48f5740bf30bb3468ca532cb7627997)) +* **popup:** manual doautocmd BufWinEnter ([0807a9c](https://github.com/MunifTanjim/nui.nvim/commit/0807a9ca3274d5f89d02802aa00dac9fc2649864)) +* **popup:** position relative to buffer position ([f369333](https://github.com/MunifTanjim/nui.nvim/commit/f36933397a0689a96106d9aa74db320286f5ffda)) +* **popup:** properly apply winhighlight ([4396e44](https://github.com/MunifTanjim/nui.nvim/commit/4396e4427dc7ec80c58575096fdcad46d336a7ac)) +* **popup:** remove mutation in border:get() ([1179f2e](https://github.com/MunifTanjim/nui.nvim/commit/1179f2e3f5245ab585e1154478f45bab1cf41867)) +* **popup:** respect 'enter' option on subsequent mounts ([602e4d8](https://github.com/MunifTanjim/nui.nvim/commit/602e4d885fda5da9f015345fecb8c745ffedbf52)) +* **popup:** set noautocmd for opening border window ([4715f60](https://github.com/MunifTanjim/nui.nvim/commit/4715f6092443f0b8fb9a3bcb0cfd03202bb03477)) +* **popup:** set win_config.win explicitly if applicable ([d50d84a](https://github.com/MunifTanjim/nui.nvim/commit/d50d84a340a3088b8560bf74fa3d6f6ad0556134)) +* **popup:** store winid for parent window ([03131f8](https://github.com/MunifTanjim/nui.nvim/commit/03131f8aa1873d08009ca214727a2c699d73dfe6)) +* **popup:** take border size_delta into account when calculating position ([e67310b](https://github.com/MunifTanjim/nui.nvim/commit/e67310b23d21ebe8b12d9dbadb3dfa562dda5057)) +* **popup:** track mounted state ([c1db7f8](https://github.com/MunifTanjim/nui.nvim/commit/c1db7f8377dfce4f724f35eec74868a15443dccf)) +* **popup:** update position handling ([d87b561](https://github.com/MunifTanjim/nui.nvim/commit/d87b56193991e102516689f394101d1367bf8ef5)) +* **popup:** update state and buffer handling for hide/show ([02e9262](https://github.com/MunifTanjim/nui.nvim/commit/02e9262d2d1820c2ed573f3a09848affc6526704)) +* **popup:** use copy of border styles table item ([abd1a4a](https://github.com/MunifTanjim/nui.nvim/commit/abd1a4a23d0bbc0d0d4bc8faaa240330b44a4d5c)) +* **popup:** use popup winhighlight for border ([cf67636](https://github.com/MunifTanjim/nui.nvim/commit/cf676363181aae149271226531abca341e583997)) +* **split:** check bufnr is valid before clearing namespace ([e9889bb](https://github.com/MunifTanjim/nui.nvim/commit/e9889bbd9919544697d497537acacd9c67d0de99)) +* **split:** do buf_storage.cleanup after buffer wipeout ([5bf5d62](https://github.com/MunifTanjim/nui.nvim/commit/5bf5d62531b473b01cae65ea389d4f788fb2341f)) +* **split:** do not update position when unchanged ([6d86148](https://github.com/MunifTanjim/nui.nvim/commit/6d86148ad43554f51caa82e99d63a57be1654182)) +* **split:** set size after open window ([c45ad68](https://github.com/MunifTanjim/nui.nvim/commit/c45ad685cb156174761f912c7697fdb6fd5a3b50)) +* **split:** skip manual buf/win removal when pending quit ([cc76e6f](https://github.com/MunifTanjim/nui.nvim/commit/cc76e6ff13629b18d3dedfadd4f52e35ff085700)) +* **split:** tweak container size relative to editor ([120fe69](https://github.com/MunifTanjim/nui.nvim/commit/120fe69bc4d96a13f89c2450ceb27fcd921b77ae)) +* **split:** use self.winid for WinClosed ([747c20d](https://github.com/MunifTanjim/nui.nvim/commit/747c20d10a42f22a0125ee2e7397aee4c2422ffe)) +* support nui.text for internal alignment util ([915fabe](https://github.com/MunifTanjim/nui.nvim/commit/915fabe334639c384ed5c66005046b96d4e8d30a)) +* **text:** fix :highlight extmark.end_col ([3775746](https://github.com/MunifTanjim/nui.nvim/commit/3775746f8324db1ff5068eaf10f321db5e566611)) +* **tree:** fix calculation for method :render ([70f2dad](https://github.com/MunifTanjim/nui.nvim/commit/70f2dadb73b5aa15727ec8f7a620818997505be5)) +* **tree:** pass ns_id correctly to nui.line in :render method ([70fc6b6](https://github.com/MunifTanjim/nui.nvim/commit/70fc6b66c651538a78d393cf9fc830c81e919ca8)) +* **tree:** remove children recursively in method remove_node ([4939282](https://github.com/MunifTanjim/nui.nvim/commit/4939282919885e1c83aff68ecb35b3cadf6015a9)) +* **tree:** remove id from .nodes.root_ids on tree:remove_node ([792caa3](https://github.com/MunifTanjim/nui.nvim/commit/792caa3c1dc3efb22385b3b363c32567ba9cb059)) +* **tree:** set default buf_options.bufhidden=hide ([7c7bdf4](https://github.com/MunifTanjim/nui.nvim/commit/7c7bdf40219bc274a849fa241daa47872c809fd8)) +* **tree:** set default buf_options.undolevels=0 ([42552b3](https://github.com/MunifTanjim/nui.nvim/commit/42552b3797c3452c5c94e0c84a04fbda9591b9d1)) +* vim.schedule QuitPre event callback ([d147222](https://github.com/MunifTanjim/nui.nvim/commit/d147222a1300901656f3ebd5b95f91732785a329)) +* **window:** cleanup properly ([dbc8185](https://github.com/MunifTanjim/nui.nvim/commit/dbc81850faeeb70c303cdf3c50e0a4b6bbf154bd)) +* **window:** set zindex properly ([2d427f7](https://github.com/MunifTanjim/nui.nvim/commit/2d427f7f9bac670523de2906776117192f243d8c)) +* **window:** use 0-indexed position ([fbf96df](https://github.com/MunifTanjim/nui.nvim/commit/fbf96df95e437687206b9213924372c13c328b7a)) + + +### Continuous Integration + +* introduce automated release ([70560c4](https://github.com/MunifTanjim/nui.nvim/commit/70560c4e7b36ff974e7136d08aa4022e79a002f4)) diff --git a/.config/nvim/pack/tree/start/nui.nvim/LICENSE b/.config/nvim/pack/tree/start/nui.nvim/LICENSE new file mode 100644 index 0000000..6ddc2c5 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Munif Tanjim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.config/nvim/pack/tree/start/nui.nvim/README.md b/.config/nvim/pack/tree/start/nui.nvim/README.md new file mode 100644 index 0000000..058e602 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/README.md @@ -0,0 +1,353 @@ +![GitHub Workflow Status: CI](https://img.shields.io/github/actions/workflow/status/MunifTanjim/nui.nvim/ci.yml?branch=main&label=CI&style=for-the-badge) +[![Coverage](https://img.shields.io/codecov/c/gh/MunifTanjim/nui.nvim/master?style=for-the-badge)](https://codecov.io/gh/MunifTanjim/nui.nvim) +[![Version](https://img.shields.io/luarocks/v/MunifTanjim/nui.nvim?color=%232c3e67&style=for-the-badge)](https://luarocks.org/modules/MunifTanjim/nui.nvim) +![License](https://img.shields.io/github/license/MunifTanjim/nui.nvim?color=%23000080&style=for-the-badge) + +# nui.nvim + +UI Component Library for Neovim. + +## Requirements + +- [Neovim 0.5.0](https://github.com/neovim/neovim/releases/tag/v0.5.0) + +## Installation + +Install the plugins with your preferred plugin manager. For example, with [`vim-plug`](https://github.com/junegunn/vim-plug): + +```vim +Plug 'MunifTanjim/nui.nvim' +``` + +## Blocks + +### [NuiText](lua/nui/text) + +Quickly add highlighted text on the buffer. + +**[Check Detailed Documentation for `nui.text`](lua/nui/text)** + +**[Check Wiki Page for `nui.text`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.text)** + +### [NuiLine](lua/nui/line) + +Quickly add line containing highlighted text chunks on the buffer. + +**[Check Detailed Documentation for `nui.line`](lua/nui/line)** + +**[Check Wiki Page for `nui.line`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.line)** + +### [NuiTable](lua/nui/table) + +Quickly render table-like structured content on the buffer. + +**[Check Detailed Documentation for `nui.table`](lua/nui/table)** + +**[Check Wiki Page for `nui.table`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.table)** + +### [NuiTree](lua/nui/tree) + +Quickly render tree-like structured content on the buffer. + +**[Check Detailed Documentation for `nui.tree`](lua/nui/tree)** + +**[Check Wiki Page for `nui.tree`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.tree)** + +## Components + +### [Layout](lua/nui/layout) + +![Layout GIF](https://github.com/MunifTanjim/nui.nvim/wiki/media/layout.gif) + +```lua +local Popup = require("nui.popup") +local Layout = require("nui.layout") + +local popup_one, popup_two = Popup({ + enter = true, + border = "single", +}), Popup({ + border = "double", +}) + +local layout = Layout( + { + position = "50%", + size = { + width = 80, + height = "60%", + }, + }, + Layout.Box({ + Layout.Box(popup_one, { size = "40%" }), + Layout.Box(popup_two, { size = "60%" }), + }, { dir = "row" }) +) + +local current_dir = "row" + +popup_one:map("n", "r", function() + if current_dir == "col" then + layout:update(Layout.Box({ + Layout.Box(popup_one, { size = "40%" }), + Layout.Box(popup_two, { size = "60%" }), + }, { dir = "row" })) + + current_dir = "row" + else + layout:update(Layout.Box({ + Layout.Box(popup_two, { size = "60%" }), + Layout.Box(popup_one, { size = "40%" }), + }, { dir = "col" })) + + current_dir = "col" + end +end, {}) + +layout:mount() +``` + +**[Check Detailed Documentation for `nui.layout`](lua/nui/layout)** + +**[Check Wiki Page for `nui.layout`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.layout)** + +### [Popup](lua/nui/popup) + +![Popup GIF](https://github.com/MunifTanjim/nui.nvim/wiki/media/popup.gif) + +```lua +local Popup = require("nui.popup") +local event = require("nui.utils.autocmd").event + +local popup = Popup({ + enter = true, + focusable = true, + border = { + style = "rounded", + }, + position = "50%", + size = { + width = "80%", + height = "60%", + }, +}) + +-- mount/open the component +popup:mount() + +-- unmount component when cursor leaves buffer +popup:on(event.BufLeave, function() + popup:unmount() +end) + +-- set content +vim.api.nvim_buf_set_lines(popup.bufnr, 0, 1, false, { "Hello World" }) +``` + +**[Check Detailed Documentation for `nui.popup`](lua/nui/popup)** + +**[Check Wiki Page for `nui.popup`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.popup)** + +### [Input](lua/nui/input) + +![Input GIF](https://github.com/MunifTanjim/nui.nvim/wiki/media/input.gif) + +```lua +local Input = require("nui.input") +local event = require("nui.utils.autocmd").event + +local input = Input({ + position = "50%", + size = { + width = 20, + }, + border = { + style = "single", + text = { + top = "[Howdy?]", + top_align = "center", + }, + }, + win_options = { + winhighlight = "Normal:Normal,FloatBorder:Normal", + }, +}, { + prompt = "> ", + default_value = "Hello", + on_close = function() + print("Input Closed!") + end, + on_submit = function(value) + print("Input Submitted: " .. value) + end, +}) + +-- mount/open the component +input:mount() + +-- unmount component when cursor leaves buffer +input:on(event.BufLeave, function() + input:unmount() +end) +``` + +**[Check Detailed Documentation for `nui.input`](lua/nui/input)** + +**[Check Wiki Page for `nui.input`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.input)** + +### [Menu](lua/nui/menu) + +![Menu GIF](https://github.com/MunifTanjim/nui.nvim/wiki/media/menu.gif) + +```lua +local Menu = require("nui.menu") +local event = require("nui.utils.autocmd").event + +local menu = Menu({ + position = "50%", + size = { + width = 25, + height = 5, + }, + border = { + style = "single", + text = { + top = "[Choose-an-Element]", + top_align = "center", + }, + }, + win_options = { + winhighlight = "Normal:Normal,FloatBorder:Normal", + }, +}, { + lines = { + Menu.item("Hydrogen (H)"), + Menu.item("Carbon (C)"), + Menu.item("Nitrogen (N)"), + Menu.separator("Noble-Gases", { + char = "-", + text_align = "right", + }), + Menu.item("Helium (He)"), + Menu.item("Neon (Ne)"), + Menu.item("Argon (Ar)"), + }, + max_width = 20, + keymap = { + focus_next = { "j", "", "" }, + focus_prev = { "k", "", "" }, + close = { "", "" }, + submit = { "", "" }, + }, + on_close = function() + print("Menu Closed!") + end, + on_submit = function(item) + print("Menu Submitted: ", item.text) + end, +}) + +-- mount the component +menu:mount() +``` + +**[Check Detailed Documentation for `nui.menu`](lua/nui/menu)** + +**[Check Wiki Page for `nui.menu`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.menu)** + +### [Split](lua/nui/split) + +![Split GIF](https://github.com/MunifTanjim/nui.nvim/wiki/media/split.gif) + +```lua +local Split = require("nui.split") +local event = require("nui.utils.autocmd").event + +local split = Split({ + relative = "editor", + position = "bottom", + size = "20%", +}) + +-- mount/open the component +split:mount() + +-- unmount component when cursor leaves buffer +split:on(event.BufLeave, function() + split:unmount() +end) +``` + +**[Check Detailed Documentation for `nui.split`](lua/nui/split)** + +**[Check Wiki Page for `nui.split`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.split)** + +## Extendibility + +Each of the [blocks](#blocks) and [components](#components) can be extended to add new +methods or change their behaviors. + +```lua +local Timer = Popup:extend("Timer") + +function Timer:init(popup_options) + local options = vim.tbl_deep_extend("force", popup_options or {}, { + border = "double", + focusable = false, + position = { row = 0, col = "100%" }, + size = { width = 10, height = 1 }, + win_options = { + winhighlight = "Normal:Normal,FloatBorder:SpecialChar", + }, + }) + + Timer.super.init(self, options) +end + +function Timer:countdown(time, step, format) + local function draw_content(text) + local gap_width = 10 - vim.api.nvim_strwidth(text) + vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, { + string.format( + "%s%s%s", + string.rep(" ", math.floor(gap_width / 2)), + text, + string.rep(" ", math.ceil(gap_width / 2)) + ), + }) + end + + self:mount() + + local remaining_time = time + + draw_content(format(remaining_time)) + + vim.fn.timer_start(step, function() + remaining_time = remaining_time - step + + draw_content(format(remaining_time)) + + if remaining_time <= 0 then + self:unmount() + end + end, { ["repeat"] = math.ceil(remaining_time / step) }) +end + +local timer = Timer() + +timer:countdown(10000, 1000, function(time) + return tostring(time / 1000) .. "s" +end) +``` + +#### `nui.object` + +A small object library is bundled with `nui.nvim`. It is, more or less, a clone of the +[`kikito/middleclass`](https://github.com/kikito/middleclass) library. + +[Check Wiki Page for `nui.object`](https://github.com/MunifTanjim/nui.nvim/wiki/nui.object) + +## License + +Licensed under the MIT License. Check the [LICENSE](./LICENSE) file for details. diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/input/README.md b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/input/README.md new file mode 100644 index 0000000..34f01dc --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/input/README.md @@ -0,0 +1,114 @@ +# Input + +Input is an abstraction layer on top of Popup. + +It uses prompt buffer (check `:h prompt-buffer`) for its popup window. + +```lua +local Input = require("nui.input") +local event = require("nui.utils.autocmd").event + +local popup_options = { + relative = "cursor", + position = { + row = 1, + col = 0, + }, + size = 20, + border = { + style = "rounded", + text = { + top = "[Input]", + top_align = "left", + }, + }, + win_options = { + winhighlight = "Normal:Normal", + }, +} + +local input = Input(popup_options, { + prompt = "> ", + default_value = "42", + on_close = function() + print("Input closed!") + end, + on_submit = function(value) + print("Value submitted: ", value) + end, + on_change = function(value) + print("Value changed: ", value) + end, +}) +``` + +If you provide the `on_change` function, it'll be run everytime value changes. + +Pressing `` runs the `on_submit` callback function and closes the window. +Pressing `` runs the `on_close` callback function and closes the window. + +Of course, you can override the default keymaps and add more. For example: + +```lua +-- unmount input by pressing `` in normal mode +input:map("n", "", function() + input:unmount() +end, { noremap = true }) +``` + +You can manipulate the associated buffer and window using the +`input.bufnr` and `input.winid` properties. + +**NOTE**: the first argument accepts options for `nui.popup` component. + +## Options + +### `prompt` + +**Type:** `string` or `NuiText` + +Prefix in the input. + +### `default_value` + +**Type:** `string` + +Default value placed in the input on mount + +### `on_close` + +**Type:** `function` + +_Signature:_ `on_close() -> nil` + +Callback function, called when input is closed. + +### `on_submit` + +**Type:** `function` + +_Signature:_ `on_submit(value: string) -> nil` + +Callback function, called when input value is submitted. + +### `on_change` + +**Type:** `function` + +_Signature:_ `on_change(value: string) -> nil` + +Callback function, called when input value is changed. + +### `disable_cursor_position_patch` + +By default, `nui.input` will try to make sure the cursor on parent window is not +moved after input is submitted/closed. If you want to disable this behavior +for some reason, you can set `disable_cursor_position_patch` to `true`. + +## Methods + +Methods from `nui.popup` are also available for `nui.input`. + +## Wiki Page + +You can find additional documentation/examples/guides/tips-n-tricks in [nui.input wiki page](https://github.com/MunifTanjim/nui.nvim/wiki/nui.input). diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/input/init.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/input/init.lua new file mode 100644 index 0000000..bfae51e --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/input/init.lua @@ -0,0 +1,174 @@ +local Popup = require("nui.popup") +local Text = require("nui.text") +local defaults = require("nui.utils").defaults +local is_type = require("nui.utils").is_type +local event = require("nui.utils.autocmd").event + +-- exiting insert mode places cursor one character backward, +-- so patch the cursor position to one character forward +-- when unmounting input. +---@param target_cursor number[] +---@param force? boolean +local function patch_cursor_position(target_cursor, force) + local cursor = vim.api.nvim_win_get_cursor(0) + + if target_cursor[2] == cursor[2] and force then + -- didn't exit insert mode yet, but it's gonna + vim.api.nvim_win_set_cursor(0, { cursor[1], cursor[2] + 1 }) + elseif target_cursor[2] - 1 == cursor[2] then + -- already exited insert mode + vim.api.nvim_win_set_cursor(0, { cursor[1], cursor[2] + 1 }) + end +end + +---@class nui_input_options +---@field prompt? string|NuiText +---@field default_value? string +---@field on_change? fun(value: string): nil +---@field on_close? fun(): nil +---@field on_submit? fun(value: string): nil + +---@class nui_input_internal: nui_popup_internal +---@field default_value string +---@field prompt NuiText +---@field disable_cursor_position_patch boolean +---@field on_change? fun(value: string): nil +---@field on_close fun(): nil +---@field on_submit fun(value: string): nil +---@field pending_submit_value? string + +---@class NuiInput: NuiPopup +---@field private _ nui_input_internal +local Input = Popup:extend("NuiInput") + +---@param popup_options nui_popup_options +---@param options nui_input_options +function Input:init(popup_options, options) + popup_options.enter = false + + popup_options.buf_options = defaults(popup_options.buf_options, {}) + popup_options.buf_options.buftype = "prompt" + + if not is_type("table", popup_options.size) then + popup_options.size = { + width = popup_options.size, + } + end + + popup_options.size.height = 1 + + Input.super.init(self, popup_options) + + self._.default_value = defaults(options.default_value, "") + self._.prompt = Text(defaults(options.prompt, "")) + self._.disable_cursor_position_patch = defaults(options.disable_cursor_position_patch, false) + + self.input_props = {} + + self._.on_change = options.on_change + self._.on_close = options.on_close or function() end + self._.on_submit = options.on_submit or function() end +end + +function Input:mount() + local props = self.input_props + + if self._.mounted then + return + end + + vim.fn.prompt_setprompt(self.bufnr, self._.prompt:content()) + + vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, { self._.prompt:content() .. self._.default_value }) + + self:on(event.BufWinEnter, function() + vim.schedule(function() + if self._.prompt:length() > 0 then + self._.prompt:highlight(self.bufnr, self.ns_id, 1, 0) + end + + vim.api.nvim_set_current_win(self.winid) + end) + + vim.api.nvim_command("startinsert!") + end, { once = false }) + + Input.super.mount(self) + + if self._.on_change then + ---@deprecated + props.on_change = function() + local value_with_prompt = vim.api.nvim_buf_get_lines(self.bufnr, 0, 1, false)[1] + local value = string.sub(value_with_prompt, self._.prompt:length() + 1) + self._.on_change(value) + end + + vim.api.nvim_buf_attach(self.bufnr, false, { + on_lines = props.on_change, + }) + end + + ---@deprecated + props.on_submit = function(value) + self._.pending_submit_value = value + self:unmount() + end + + vim.fn.prompt_setcallback(self.bufnr, props.on_submit) + + -- @deprecated + --- Use `input:unmount` + ---@deprecated + props.on_close = function() + self:unmount() + end + + vim.fn.prompt_setinterrupt(self.bufnr, props.on_close) +end + +function Input:unmount() + if not self._.mounted then + return + end + + local container_winid = self._.container_info.winid + local target_cursor = vim.api.nvim_win_is_valid(container_winid) and vim.api.nvim_win_get_cursor(container_winid) + or nil + local prompt_mode = vim.fn.mode() + + Input.super.unmount(self) + + if self._.loading then + return + end + + self._.loading = true + + local pending_submit_value = self._.pending_submit_value + + vim.schedule(function() + -- NOTE: on prompt-buffer normal mode causes neovim to enter insert mode. + -- ref: https://github.com/neovim/neovim/blob/d8f5f4d09078/src/nvim/normal.c#L5327-L5333 + if (pending_submit_value and prompt_mode == "n") or prompt_mode == "i" then + vim.api.nvim_command("stopinsert") + end + + if not self._.disable_cursor_position_patch and target_cursor ~= nil then + patch_cursor_position(target_cursor, pending_submit_value and prompt_mode == "n") + end + + if pending_submit_value then + self._.pending_submit_value = nil + self._.on_submit(pending_submit_value) + else + self._.on_close() + end + self._.loading = false + end) +end + +---@alias NuiInput.constructor fun(popup_options: nui_popup_options, options: nui_input_options): NuiInput +---@type NuiInput|NuiInput.constructor +local NuiInput = Input + +return NuiInput diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/README.md b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/README.md new file mode 100644 index 0000000..6882a23 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/README.md @@ -0,0 +1,307 @@ +# Layout + +Layout is a helper component for creating complex layout by automatically +handling the calculation for position and size of other components. + +**Example** + +```lua +local Layout = require("nui.layout") +local Popup = require("nui.popup") + +local top_popup = Popup({ border = "double" }) +local bottom_left_popup = Popup({ border = "single" }) +local bottom_right_popup = Popup({ border = "single" }) + +local layout = Layout( + { + position = "50%", + size = { + width = 80, + height = 40, + }, + }, + Layout.Box({ + Layout.Box(top_popup, { size = "40%" }), + Layout.Box({ + Layout.Box(bottom_left_popup, { size = "50%" }), + Layout.Box(bottom_right_popup, { size = "50%" }), + }, { dir = "row", size = "60%" }), + }, { dir = "col" }) +) + +layout:mount() +``` + +_Signature:_ `Layout(options, box)` or `Layout(component, box)` + +`component` can be `Popup` or `Split`. + +## Options (for float layout) + +### `anchor` + +**Type:** `"NW"` / `"NE"` / `"SW"` / `"SE"` + +Decides which corner of the layout to place at `position`. + +--- + +### `relative` + +**Type:** `string` or `table` + +This option affects how `position` and `size` are calculated. + +**Examples** + +Relative to cursor on current window: + +```lua +relative = "cursor", +``` + +Relative to the current editor screen: + +```lua +relative = "editor", +``` + +Relative to the current window (_default_): + +```lua +relative = "win", +``` + +Relative to the window with specific id: + +```lua +relative = { + type = "win", + winid = 5, +}, +``` + +Relative to the buffer position: + +```lua +relative = { + type = "buf", + -- zero-indexed + position = { + row = 5, + col = 5, + }, +}, +``` + +--- + +### `position` + +**Type:** `number` or `percentage string` or `table` + +Position is calculated from the top-left corner. + +If `position` is `number` or `percentage string`, it applies to both `row` and `col`. +Or you can pass a table to set them separately. + +For `percentage string`, position is calculated according to the option `relative`. +If `relative` is set to `"buf"` or `"cursor"`, `percentage string` is not allowed. + +**Examples** + +```lua +position = 50, +``` + +```lua +position = "50%", +``` + +```lua +position = { + row = 30, + col = 20, +}, +``` + +```lua +position = { + row = "20%", + col = "50%", +}, +``` + +--- + +### `size` + +**Type:** `number` or `percentage string` or `table` + +Determines the size of the layout. + +If `size` is `number` or `percentage string`, it applies to both `width` and `height`. +You can also pass a table to set them separately. + +For `percentage string`, `size` is calculated according to the option `relative`. +If `relative` is set to `"buf"` or `"cursor"`, window size is considered. + +Decimal `number` in `(0,1)` range is treated similar to `percentage string`. For +example: `0.5` is same as `"50%"`. + +**Examples** + +```lua +size = 50, +``` + +```lua +size = "50%", +``` + +```lua +size = 0.5, +``` + +```lua +size = { + width = 80, + height = 40, +}, +``` + +```lua +size = { + width = "80%", + height = 0.6, +}, +``` + +## Options (for split layout) + +### `relative` + +**Type:** `string` or `table` + +This option affects how `size` is calculated. + +**Examples** + +Split current editor screen: + +```lua +relative = "editor" +``` + +Split current window (_default_): + +```lua +relative = "win" +``` + +Split window with specific id: + +```lua +relative = { + type = "win", + winid = 42, +} +``` + +--- + +### `position` + +**Type:** `"top" | "right"| "bottom" | "left"`. + +--- + +### `size` + +**Type:** `number` or `percentage string` + +Determines the size of the layout. + +For `percentage string`, size is calculated according to the option `relative`. + +## Layout.Box + +_Signature:_ `Layout.Box(box, options)` + +**Parameters** + +| Name | Type | Description | +| --------- | ------------------------------ | ----------------------------------------- | +| `box` | `Layout.Box[]` / nui component | list of `Layout.Box` or any nui component | +| `options` | `table` | box options | + +`options` is a `table` having the following keys: + +| Key | Type | Description | +| ------ | ----------------------------- | ------------------------------------------------------ | +| `dir` | `"col"` / `"row"` (_default_) | arrangement direction, only if `box` is `Layout.Box[]` | +| `grow` | `number` | growth factor to fill up the box free space | +| `size` | `number` / `string` / `table` | optional if `grow` is present | + +## Methods + +### `layout:mount` + +_Signature:_ `layout:mount()` + +Mounts the layout with all the components. + +**Examples** + +```lua +layout:mount() +``` + +### `layout:unmount` + +_Signature:_ `layout:unmount()` + +Unmounts the layout with all the components. + +**Examples** + +```lua +layout:unmount() +``` + +### `layout:hide` + +_Signature:_ `layout:hide()` + +Hides the layout with all the components. Preserves the buffer (related content, autocmds and keymaps). + +### `layout:show` + +_Signature:_ `layout:show()` + +Shows the hidden layout with all the components. + +### `layout:update` + +_Signature:_ `layout:update(config, box?)` or `layout:update(box?)` + +**Parameters** + +`config` is a `table` having the following keys: + +| Key | Type | +| ---------- | --------------------------------- | +| `anchor` | `"NW"` / `"NE"` / `"SW"` / `"SE"` | +| `relative` | `string` / `table` | +| `position` | `string` / `table` | +| `size` | `string` / `table` | + +`box` is a `table` returned by `Layout.Box`. + +They are the same options used for layout initialization. + +## Wiki Page + +You can find additional documentation/examples/guides/tips-n-tricks in +[nui.layout wiki page](https://github.com/MunifTanjim/nui.nvim/wiki/nui.layout). diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/float.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/float.lua new file mode 100644 index 0000000..cc9e995 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/float.lua @@ -0,0 +1,239 @@ +local utils = require("nui.utils") +local layout_utils = require("nui.layout.utils") + +local u = { + is_type = utils.is_type, + calculate_window_size = layout_utils.calculate_window_size, +} + +local mod = {} + +local function get_child_position(box, child, current_position, canvas_position) + local position = box.dir == "row" and { + row = canvas_position.row, + col = current_position.col, + } or { + col = canvas_position.col, + row = current_position.row, + } + + if child.component then + local border = child.component.border + if border and border._.type == "complex" then + position.col = position.col + math.floor(border._.size_delta.width / 2 + 0.5) + position.row = position.row + math.floor(border._.size_delta.height / 2 + 0.5) + end + end + + return position +end + +---@param parent table Layout.Box +---@param child table Layout.Box +---@param container_size table +---@param growable_dimension_per_factor? number +local function get_child_size(parent, child, container_size, growable_dimension_per_factor) + local child_size = { + width = child.size.width, + height = child.size.height, + } + + if child.grow and growable_dimension_per_factor then + if parent.dir == "col" then + child_size.height = math.floor(growable_dimension_per_factor * child.grow) + else + child_size.width = math.floor(growable_dimension_per_factor * child.grow) + end + end + + local outer_size = u.calculate_window_size(child_size, container_size) + + local inner_size = { + width = outer_size.width, + height = outer_size.height, + } + + if child.component then + if child.component.border then + inner_size.width = inner_size.width - child.component.border._.size_delta.width + inner_size.height = inner_size.height - child.component.border._.size_delta.height + + if inner_size.height <= 0 then + local height_adjustment = math.abs(inner_size.height) + 1 + inner_size.height = inner_size.height + height_adjustment + outer_size.height = outer_size.height + height_adjustment + end + end + end + + return outer_size, inner_size +end + +function mod.process(box, meta) + -- luacov: disable + if box.mount or box.component or not box.box then + return error("invalid parameter: box") + end + -- luacov: enable + + local container_size = meta.container_size + + -- luacov: disable + if not u.is_type("number", container_size.width) or not u.is_type("number", container_size.height) then + return error("invalid value: box.size") + end + -- luacov: enable + + local current_position = box.dir == "row" and { + col = meta.position.col, + row = 0, + } or { + col = 0, + row = meta.position.row, + } + + local growable_child_factor = 0 + + for _, child in ipairs(box.box) do + if meta.process_growable_child or not child.grow then + local position = get_child_position(box, child, current_position, meta.position) + local outer_size, inner_size = get_child_size(box, child, container_size, meta.growable_dimension_per_factor) + + if child.component then + child.component:set_layout({ + size = inner_size, + relative = { + type = "win", + winid = meta.winid, + }, + position = position, + }) + else + mod.process(child, { + winid = meta.winid, + container_size = outer_size, + position = position, + }) + end + + current_position.col = current_position.col + outer_size.width + current_position.row = current_position.row + outer_size.height + end + + if child.grow then + growable_child_factor = growable_child_factor + child.grow + end + end + + if meta.process_growable_child or growable_child_factor == 0 then + return + end + + local growable_width = container_size.width - current_position.col + local growable_height = container_size.height - current_position.row + local growable_dimension = box.dir == "col" and growable_height or growable_width + local growable_dimension_per_factor = growable_dimension / growable_child_factor + + mod.process(box, { + winid = meta.winid, + container_size = meta.container_size, + position = meta.position, + process_growable_child = true, + growable_dimension_per_factor = growable_dimension_per_factor, + }) +end + +function mod.mount_box(box) + for _, child in ipairs(box.box) do + if child.component then + child.component:mount() + else + mod.mount_box(child) + end + end +end + +---@param box table Layout.Box +function mod.show_box(box) + for _, child in ipairs(box.box) do + if child.component then + child.component:show() + else + mod.show_box(child) + end + end +end + +function mod.unmount_box(box) + for _, child in ipairs(box.box) do + if child.component then + child.component:unmount() + else + mod.unmount_box(child) + end + end +end + +---@param box table Layout.Box +function mod.hide_box(box) + for _, child in ipairs(box.box) do + if child.component then + child.component:hide() + else + mod.hide_box(child) + end + end +end + +---@param box table Layout.Box +---@return table +local function collect_box_components(box, components) + if not components then + components = {} + end + + for _, child in ipairs(box.box) do + if child.component then + components[child.component._.id] = child.component + else + collect_box_components(child, components) + end + end + + return components +end + +---@param curr_box table Layout.Box +---@param prev_box table Layout.Box +function mod.process_box_change(curr_box, prev_box) + if curr_box == prev_box then + return + end + + local curr_components = collect_box_components(curr_box) + local prev_components = collect_box_components(prev_box) + + for id, component in pairs(curr_components) do + if not prev_components[id] then + if not component.winid then + if component._.mounted then + component:show() + else + component:mount() + end + end + end + end + + for id, component in pairs(prev_components) do + if not curr_components[id] then + if component._.mounted then + if component.winid then + component:hide() + end + end + end + end +end + +return mod diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/init.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/init.lua new file mode 100644 index 0000000..9436851 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/init.lua @@ -0,0 +1,573 @@ +local Object = require("nui.object") +local Popup = require("nui.popup") +local Split = require("nui.split") +local utils = require("nui.utils") +local layout_utils = require("nui.layout.utils") +local float_layout = require("nui.layout.float") +local split_layout = require("nui.layout.split") +local split_utils = require("nui.split.utils") +local autocmd = require("nui.utils.autocmd") + +local _ = utils._ + +local defaults = utils.defaults +local is_type = utils.is_type +local u = { + get_next_id = _.get_next_id, + position = layout_utils.position, + size = layout_utils.size, + split = split_utils, + update_layout_config = layout_utils.update_layout_config, +} + +-- GitHub Issue: https://github.com/neovim/neovim/issues/18925 +local function apply_workaround_for_float_relative_position_issue_18925(layout) + local winids_len = 1 + local winids = { layout.winid } + local function collect_anchor_winids(box) + for _, child in ipairs(box.box) do + if child.component then + local border = child.component.border + if border and border.winid then + winids_len = winids_len + 1 + winids[winids_len] = border.winid + end + else + collect_anchor_winids(child) + end + end + end + collect_anchor_winids(layout._.box) + + vim.schedule(function() + -- check in case layout was immediately hidden or unmounted + if layout.winid == winids[1] and vim.api.nvim_win_is_valid(winids[1]) then + vim.cmd( + ("noa call nvim_set_current_win(%s)\nnormal! jk\nredraw\n"):rep(winids_len):format(unpack(winids)) + .. ("noa call nvim_set_current_win(%s)"):format(vim.api.nvim_get_current_win()) + ) + end + end) +end + +---@param options nui_layout_options +local function merge_default_options(options) + options.relative = defaults(options.relative, "win") + + return options +end + +---@param options nui_layout_options +local function normalize_options(options) + options = _.normalize_layout_options(options) + + return options +end + +---@return boolean +local function is_box(object) + return object and (object.box or object.component) +end + +---@return boolean +local function is_component(object) + return object and object.mount +end + +local function is_component_mounted(component) + return is_type("number", component.winid) +end + +---@param component NuiPopup|NuiSplit +local function get_layout_config_relative_to_component(component) + return { + relative = { type = "win", winid = component.winid }, + position = { row = 0, col = 0 }, + size = { width = "100%", height = "100%" }, + } +end + +---@param layout NuiLayout +---@param box table Layout.Box +local function wire_up_layout_components(layout, box) + for _, child in ipairs(box.box) do + if child.component then + autocmd.create({ "BufWipeout", "QuitPre" }, { + group = layout._.augroup.unmount, + buffer = child.component.bufnr, + callback = vim.schedule_wrap(function() + layout:unmount() + end), + }, child.component.bufnr) + + autocmd.create("BufWinEnter", { + group = layout._.augroup.unmount, + buffer = child.component.bufnr, + callback = function() + local winid = child.component.winid + if layout._.type == "float" and not winid then + --[[ + `BufWinEnter` does not contain window id and + it is fired before `nvim_open_win` returns + the window id. + --]] + winid = vim.fn.bufwinid(child.component.bufnr) + end + + autocmd.create("WinClosed", { + group = layout._.augroup.hide, + nested = true, + pattern = tostring(winid), + callback = function() + layout:hide() + end, + }, child.component.bufnr) + end, + }, child.component.bufnr) + else + wire_up_layout_components(layout, child) + end + end +end + +---@class nui_layout_options +---@field anchor? nui_layout_option_anchor +---@field relative? nui_layout_option_relative_type|nui_layout_option_relative +---@field position? number|string|nui_layout_option_position +---@field size? number|string|nui_layout_option_size + +---@class NuiLayout +local Layout = Object("NuiLayout") + +---@return '"float"'|'"split"' layout_type +local function get_layout_type(box) + for _, child in ipairs(box.box) do + if child.component and child.type then + return child.type + end + + local type = get_layout_type(child) + if type then + return type + end + end + + error("unexpected empty box") +end + +---@param options nui_layout_options|NuiPopup|NuiSplit +---@param box NuiLayout.Box|NuiLayout.Box[] +function Layout:init(options, box) + local id = u.get_next_id() + + box = Layout.Box(box) + + local type = get_layout_type(box) + + self._ = { + id = id, + type = type, + box = box, + loading = false, + mounted = false, + augroup = { + hide = string.format("%s_hide", id), + unmount = string.format("%s_unmount", id), + }, + } + + if type == "float" then + local container + if is_component(options) then + container = options --[[@as NuiPopup|NuiSplit]] + options = get_layout_config_relative_to_component(container) + else + ---@cast options -NuiPopup, -NuiSplit + options = merge_default_options(options) + options = normalize_options(options) + end + + self._[type] = { + container = container, + layout = {}, + win_enter = false, + win_config = { + border = "none", + focusable = false, + style = "minimal", + anchor = options.anchor, + zindex = 49, + }, + win_options = { + winblend = 100, + }, + } + + if not is_component(container) or is_component_mounted(container) then + self:update(options) + end + end + + if type == "split" then + options = u.split.merge_default_options(options) + options = u.split.normalize_options(options) + + self._[type] = { + layout = {}, + position = options.position, + size = {}, + win_config = { + pending_changes = {}, + }, + } + + self:update(options) + end +end + +function Layout:_process_layout() + local type = self._.type + + if type == "float" then + local info = self._.float + + float_layout.process(self._.box, { + winid = self.winid, + container_size = info.size, + position = { + row = 0, + col = 0, + }, + }) + + return + end + + if type == "split" then + local info = self._.split + + split_layout.process(self._.box, { + position = info.position, + relative = info.relative, + container_size = info.size, + container_fallback_size = info.container_info.size, + }) + end +end + +function Layout:_open_window() + if self._.type == "float" then + local info = self._.float + + self.winid = vim.api.nvim_open_win(self.bufnr, info.win_enter, info.win_config) + assert(self.winid, "failed to create popup window") + + _.set_win_options(self.winid, info.win_options) + end +end + +function Layout:_close_window() + if not self.winid then + return + end + + if vim.api.nvim_win_is_valid(self.winid) then + vim.api.nvim_win_close(self.winid, true) + end + + self.winid = nil +end + +function Layout:mount() + if self._.loading or self._.mounted then + return + end + + self._.loading = true + + local type = self._.type + + if type == "float" then + local info = self._.float + + local container = info.container + if is_component(container) and not is_component_mounted(container) then + container:mount() + self:update(get_layout_config_relative_to_component(container)) + end + + if not self.bufnr then + self.bufnr = vim.api.nvim_create_buf(false, true) + assert(self.bufnr, "failed to create buffer") + end + + self:_open_window() + end + + self:_process_layout() + + if type == "float" then + float_layout.mount_box(self._.box) + + apply_workaround_for_float_relative_position_issue_18925(self) + end + + if type == "split" then + split_layout.mount_box(self._.box) + end + + self._.loading = false + self._.mounted = true +end + +function Layout:unmount() + if self._.loading or not self._.mounted then + return + end + + pcall(autocmd.delete_group, self._.augroup.hide) + pcall(autocmd.delete_group, self._.augroup.unmount) + + self._.loading = true + + local type = self._.type + + if type == "float" then + float_layout.unmount_box(self._.box) + + if self.bufnr then + if vim.api.nvim_buf_is_valid(self.bufnr) then + vim.api.nvim_buf_delete(self.bufnr, { force = true }) + end + self.bufnr = nil + end + + self:_close_window() + end + + if type == "split" then + split_layout.unmount_box(self._.box) + end + + self._.loading = false + self._.mounted = false +end + +function Layout:hide() + if self._.loading or not self._.mounted then + return + end + + self._.loading = true + + pcall(autocmd.delete_group, self._.augroup.hide) + + local type = self._.type + + if type == "float" then + float_layout.hide_box(self._.box) + + self:_close_window() + end + + if type == "split" then + split_layout.hide_box(self._.box) + end + + self._.loading = false +end + +function Layout:show() + if self._.loading then + return + end + + if not self._.mounted then + return self:mount() + end + + self._.loading = true + + autocmd.create_group(self._.augroup.hide, { clear = true }) + + local type = self._.type + + if type == "float" then + self:_open_window() + end + + self:_process_layout() + + if type == "float" then + float_layout.show_box(self._.box) + + apply_workaround_for_float_relative_position_issue_18925(self) + end + + if type == "split" then + split_layout.show_box(self._.box) + end + + self._.loading = false +end + +---@param config? NuiLayout.Box|NuiLayout.Box[]|nui_layout_options +---@param box? NuiLayout.Box +function Layout:update(config, box) + config = config or {} --[[@as nui_layout_options]] + + if not box and is_box(config) or is_box(config[1]) then + box = config --[=[@as NuiLayout.Box|NuiLayout.Box[]]=] + ---@type nui_layout_options + config = {} + end + + autocmd.create_group(self._.augroup.hide, { clear = true }) + autocmd.create_group(self._.augroup.unmount, { clear = true }) + + local prev_box = self._.box + + if box then + self._.box = Layout.Box(box) + self._.type = get_layout_type(self._.box) + end + + if self._.type == "float" then + local info = self._.float + + u.update_layout_config(info, config) + + if self.winid then + vim.api.nvim_win_set_config(self.winid, info.win_config) + + self:_process_layout() + + float_layout.process_box_change(self._.box, prev_box) + + apply_workaround_for_float_relative_position_issue_18925(self) + end + + wire_up_layout_components(self, self._.box) + end + + if self._.type == "split" then + local info = self._.split + + local relative_winid = info.relative and info.relative.win + + local prev_winid = vim.api.nvim_get_current_win() + if relative_winid then + vim.api.nvim_set_current_win(relative_winid) + end + + local curr_box = self._.box + if prev_box ~= curr_box then + self._.box = prev_box + self:hide() + self._.box = curr_box + end + + u.split.update_layout_config(info, config) + + if prev_box == curr_box then + self:_process_layout() + else + self:show() + end + + if vim.api.nvim_win_is_valid(prev_winid) then + vim.api.nvim_set_current_win(prev_winid) + end + + wire_up_layout_components(self, self._.box) + end +end + +---@class nui_layout_box_options +---@field dir? 'row'|'col' +---@field grow? integer +---@field size? number|string|table<'height'|'width', number|string> + +---@class NuiLayout.Box +---@field type? 'float'|'split' +---@field component? NuiPopup|NuiSplit +---@field box? NuiLayout.Box[] +---@field grow? integer +---@field size? nui_layout_option_size + +---@param box NuiPopup|NuiSplit|NuiLayout.Box|NuiLayout.Box[] +---@param options? nui_layout_box_options +---@return NuiLayout.Box +function Layout.Box(box, options) + options = options or {} + + if is_box(box) then + return box --[[@as NuiLayout.Box]] + end + + if box.mount then + local type + ---@diagnostic disable: undefined-field + if box:is_instance_of(Popup) then + type = "float" + elseif box:is_instance_of(Split) then + type = "split" + end + ---@diagnostic enable: undefined-field + + if not type then + error("unsupported component") + end + + return { + type = type, + component = box, + grow = options.grow, + size = options.size, + } + end + + local dir = defaults(options.dir, "row") + + -- normalize children size + for _, child in ipairs(box) do + if not child.grow and not child.size then + error("missing child.size") + end + + if dir == "row" then + if type(child.size) ~= "table" then + ---@diagnostic disable-next-line: assign-type-mismatch + child.size = { width = child.size } + end + if not child.size.height then + child.size.height = "100%" + end + elseif dir == "col" then + if not is_type("table", child.size) then + ---@diagnostic disable-next-line: assign-type-mismatch + child.size = { height = child.size } + end + if not child.size.width then + child.size.width = "100%" + end + end + end + + return { + box = box, + dir = dir, + grow = options.grow, + size = options.size, + } +end + +-- luacheck: push no max comment line length + +---@alias NuiLayout.constructor fun(options: nui_layout_options|NuiPopup|NuiSplit, box: NuiLayout.Box|NuiLayout.Box[]): NuiLayout +---@type NuiLayout|NuiLayout.constructor +local NuiLayout = Layout + +-- luacheck: pop + +return NuiLayout diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/split.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/split.lua new file mode 100644 index 0000000..dc2d6f2 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/split.lua @@ -0,0 +1,262 @@ +local utils = require("nui.utils") +local split_utils = require("nui.split.utils") + +local u = { + is_type = utils.is_type, + split = split_utils, + set_win_options = utils._.set_win_options, +} + +local mod = {} + +---@param box_dir '"row"'|'"col"' +---@return nui_split_internal_position position +local function get_child_position(box_dir) + if box_dir == "row" then + return "right" + else + return "bottom" + end +end + +---@param position nui_split_internal_position +---@param child { size: nui_layout_option_size, grow?: boolean } +---@param container_size { width?: number, height?: number } +---@param growable_dimension_per_factor? number +local function get_child_size(position, child, container_size, growable_dimension_per_factor) + local child_size + if position == "left" or position == "right" then + child_size = child.size.width + else + child_size = child.size.height + end + + if child.grow and growable_dimension_per_factor then + child_size = math.floor(growable_dimension_per_factor * child.grow) + end + + return u.split.calculate_window_size(position, child_size, container_size) +end + +local function get_container_size(meta) + local size = meta.container_size + size.width = size.width or meta.container_fallback_size.width + size.height = size.height or meta.container_fallback_size.height + return size +end + +function mod.process(box, meta) + -- luacov: disable + if box.mount or box.component or not box.box then + return error("invalid parameter: box") + end + -- luacov: enable + + local container_size = get_container_size(meta) + + -- luacov: disable + if not u.is_type("number", container_size.width) and not u.is_type("number", container_size.height) then + return error("invalid value: box.size") + end + -- luacov: enable + + local consumed_size = { + width = 0, + height = 0, + } + + local growable_child_factor = 0 + + for i, child in ipairs(box.box) do + if meta.process_growable_child or not child.grow then + local position = get_child_position(box.dir) + local relative = { type = "win" } + local size = get_child_size(position, child, container_size, meta.growable_dimension_per_factor) + + consumed_size.width = consumed_size.width + (size.width or 0) + consumed_size.height = consumed_size.height + (size.height or 0) + + if i == 1 then + position = meta.position + if meta.relative then + relative = meta.relative + end + if position == "left" or position == "right" then + size.width = container_size.width + else + size.height = container_size.height + end + end + + if child.component then + child.component:update_layout({ + position = position, + relative = relative, + size = size, + }) + if i == 1 and child.component.winid then + if position == "left" or position == "right" then + vim.api.nvim_win_set_height(child.component.winid, size.height) + else + vim.api.nvim_win_set_width(child.component.winid, size.width) + end + end + else + mod.process(child, { + container_size = size, + container_fallback_size = container_size, + position = position, + }) + end + end + + if child.grow then + growable_child_factor = growable_child_factor + child.grow + end + end + + if meta.process_growable_child or growable_child_factor == 0 then + return + end + + local growable_width = container_size.width - consumed_size.width + local growable_height = container_size.height - consumed_size.height + local growable_dimension = box.dir == "col" and growable_height or growable_width + local growable_dimension_per_factor = growable_dimension / growable_child_factor + + mod.process(box, { + container_size = meta.container_size, + container_fallback_size = meta.container_fallback_size, + position = meta.position, + process_growable_child = true, + growable_dimension_per_factor = growable_dimension_per_factor, + }) +end + +---@param box table Layout.Box +local function get_first_component(box) + if not box.box[1] then + return + end + + if box.box[1].component then + return box.box[1].component + end + + return get_first_component(box.box[1]) +end + +---@param box table Layout.Box +local function unset_win_options_fixsize(box) + for _, child in ipairs(box.box) do + if child.component then + local winfix = child.component._._layout_orig_winfixsize + if winfix then + child.component._.win_options.winfixwidth = winfix.winfixwidth + child.component._.win_options.winfixheight = winfix.winfixheight + child.component._._layout_orig_winfixsize = nil + end + u.set_win_options(child.component.winid, { + winfixwidth = child.component._.win_options.winfixwidth, + winfixheight = child.component._.win_options.winfixheight, + }) + else + unset_win_options_fixsize(child) + end + end +end + +---@param box table Layout.Box +---@param action '"mount"'|'"show"' +---@param meta? { initial_pass?: boolean } +local function do_action(box, action, meta) + meta = meta or { root = true } + + for i, child in ipairs(box.box) do + if not meta.initial_pass or i == 1 then + if child.component then + child.component._._layout_orig_winfixsize = { + winfixwidth = child.component._.win_options.winfixwidth, + winfixheight = child.component._.win_options.winfixheight, + } + + child.component._.win_options.winfixwidth = i ~= 1 + child.component._.win_options.winfixheight = i == 1 + if box.dir == "col" then + child.component._.win_options.winfixwidth = not child.component._.win_options.winfixwidth + child.component._.win_options.winfixheight = not child.component._.win_options.winfixheight + end + + if child.component and not child.component.winid then + child.component._.relative.win = vim.api.nvim_get_current_win() + child.component._.win_config.win = child.component._.relative.win + end + + child.component[action](child.component) + + if action == "show" and not child.component._.mounted then + child.component:mount() + end + else + do_action(child, action, { + initial_pass = true, + }) + end + end + end + + if not meta.initial_pass then + for _, child in ipairs(box.box) do + if child.box then + local first_component = get_first_component(child) + if first_component and first_component.winid then + vim.api.nvim_set_current_win(first_component.winid) + end + + do_action(child, action, { + initial_pass = false, + }) + end + end + end + + if meta.root then + unset_win_options_fixsize(box) + end +end + +---@param box table Layout.Box +---@param meta? { initial_pass?: boolean } +function mod.mount_box(box, meta) + do_action(box, "mount", meta) +end + +---@param box table Layout.Box +---@param meta? { initial_pass?: boolean } +function mod.show_box(box, meta) + do_action(box, "show", meta) +end + +---@param box table Layout.Box +function mod.unmount_box(box) + for _, child in ipairs(box.box) do + if child.component then + child.component:unmount() + else + mod.unmount_box(child) + end + end +end + +---@param box table Layout.Box +function mod.hide_box(box) + for _, child in ipairs(box.box) do + if child.component then + child.component:hide() + else + mod.hide_box(child) + end + end +end + +return mod diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/utils.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/utils.lua new file mode 100644 index 0000000..aa934c8 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/layout/utils.lua @@ -0,0 +1,226 @@ +local utils = require("nui.utils") + +local _ = utils._ +local defaults = utils.defaults + +--luacheck: push no max line length + +---@alias nui_layout_option_anchor "NW"|"NE"|"SW"|"SE" +---@alias nui_layout_option_relative_type "'cursor'"|"'editor'"|"'win'"|"'buf'" +---@alias nui_layout_option_relative { type: nui_layout_option_relative_type, winid?: number, position?: { row: number, col: number } } +---@alias nui_layout_option_position { row: number|string, col: number|string } +---@alias nui_layout_option_size { width: number|string, height: number|string } +---@alias nui_layout_internal_position { relative: "'cursor'"|"'editor'"|"'win'", win: number, bufpos?: number[], row: number, col: number } +---@alias nui_layout_container_info { relative: nui_layout_option_relative_type, size: { height: integer, width: integer }, type: "'editor'"|"'window'" } + +--luacheck: pop + +local mod_size = {} +local mod_position = {} + +local mod = { + size = mod_size, + position = mod_position, +} + +---@param position nui_layout_option_position +---@param size { width: number, height: number } +---@param container nui_layout_container_info +---@return { row: number, col: number } +function mod.calculate_window_position(position, size, container) + local row + local col + + local is_percentage_allowed = not vim.tbl_contains({ "buf", "cursor" }, container.relative) + local percentage_error = string.format("position %% can not be used relative to %s", container.relative) + + local r = utils.parse_number_input(position.row) + assert(r.value ~= nil, "invalid position.row") + if r.is_percentage then + assert(is_percentage_allowed, percentage_error) + row = math.floor((container.size.height - size.height) * r.value) + else + row = r.value + end + + local c = utils.parse_number_input(position.col) + assert(c.value ~= nil, "invalid position.col") + if c.is_percentage then + assert(is_percentage_allowed, percentage_error) + col = math.floor((container.size.width - size.width) * c.value) + else + col = c.value + end + + return { + row = row, + col = col, + } +end + +---@param size { width: number|string, height: number|string } +---@param container_size { width: number, height: number } +---@return { width: number, height: number } +function mod.calculate_window_size(size, container_size) + local width = _.normalize_dimension(size.width, container_size.width) + assert(width, "invalid size.width") + + local height = _.normalize_dimension(size.height, container_size.height) + assert(height, "invalid size.height") + + return { + width = width, + height = height, + } +end + +---@param position nui_layout_internal_position +---@return nui_layout_container_info +function mod.get_container_info(position) + local relative = position.relative + local winid = position.win == 0 and vim.api.nvim_get_current_win() or position.win + + if relative == "editor" then + return { + relative = relative, + size = utils.get_editor_size(), + type = "editor", + winid = winid, + } + end + + return { + relative = position.bufpos and "buf" or relative, + size = utils.get_window_size(position.win), + type = "window", + winid = winid, + } +end + +---@param relative nui_layout_option_relative +---@param fallback_winid number +---@return nui_layout_internal_position +function mod.parse_relative(relative, fallback_winid) + local winid = defaults(relative.winid, fallback_winid) + + if relative.type == "buf" then + return { + relative = "win", + win = winid, + bufpos = { + relative.position.row, + relative.position.col, + }, + } + end + + return { + relative = relative.type, + win = winid, + } +end + +---@param component_internal table +---@param config nui_layout_options +function mod.update_layout_config(component_internal, config) + local internal = component_internal + + local options = _.normalize_layout_options({ + relative = config.relative, + size = config.size, + position = config.position, + }) + + local win_config = internal.win_config + + if config.anchor then + win_config.anchor = config.anchor + end + + if options.relative then + internal.layout.relative = options.relative + + local fallback_winid = internal.position and internal.position.win + or internal.layout.relative.type == "cursor" and 0 + or vim.api.nvim_get_current_win() + internal.position = + vim.tbl_extend("force", internal.position or {}, mod.parse_relative(internal.layout.relative, fallback_winid)) + + win_config.relative = internal.position.relative + win_config.win = internal.position.relative == "win" and internal.position.win or nil + win_config.bufpos = internal.position.bufpos + end + + -- luacov: disable + if not win_config.relative then + return error("missing layout config: relative") + end + -- luacov: enable + + local prev_container_size = internal.container_info and internal.container_info.size + internal.container_info = mod.get_container_info(internal.position) + local container_size_changed = not mod.size.are_same(internal.container_info.size, prev_container_size) + + if + options.size + -- need_size_refresh + or (container_size_changed and internal.layout.size and mod.size.contains_percentage_string(internal.layout.size)) + then + internal.layout.size = options.size or internal.layout.size + + internal.size = mod.calculate_window_size(internal.layout.size, internal.container_info.size) + + win_config.width = internal.size.width + win_config.height = internal.size.height + end + + if not win_config.width or not win_config.height then + return error("missing layout config: size") + end + + if + options.position + -- need_position_refresh + or ( + container_size_changed + and internal.layout.position + and mod.position.contains_percentage_string(internal.layout.position) + ) + then + internal.layout.position = options.position or internal.layout.position + + internal.position = vim.tbl_extend( + "force", + internal.position, + mod.calculate_window_position(internal.layout.position, internal.size, internal.container_info) + ) + + win_config.row = internal.position.row + win_config.col = internal.position.col + end + + if not win_config.row or not win_config.col then + return error("missing layout config: position") + end +end + +---@param size_a nui_layout_option_size +---@param size_b? nui_layout_option_size +---@return boolean +function mod_size.are_same(size_a, size_b) + return size_b and size_a.width == size_b.width and size_a.height == size_b.height or false +end + +---@param size nui_layout_option_size +---@return boolean +function mod_size.contains_percentage_string(size) + return type(size.width) == "string" or type(size.height) == "string" +end + +---@param position nui_layout_option_position +---@return boolean +function mod_position.contains_percentage_string(position) + return type(position.row) == "string" or type(position.col) == "string" +end + +return mod diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/line/README.md b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/line/README.md new file mode 100644 index 0000000..77561e2 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/line/README.md @@ -0,0 +1,100 @@ +# NuiLine + +NuiLine is an abstraction layer on top of the following native functions: + +- `vim.api.nvim_buf_set_lines` (check `:h nvim_buf_set_lines()`) +- `vim.api.nvim_buf_set_text` (check `:h nvim_buf_set_text()`) +- `vim.api.nvim_buf_add_highlight` (check `:h nvim_buf_add_highlight()`) + +It helps you create line on the buffer containing multiple [`NuiText`](../text)s. + +_Signature:_ `NuiLine(texts?)` + +**Example** + +```lua +local NuiLine = require("nui.line") + +local line = NuiLine() + +line:append("Something Went Wrong!", "Error") + +local bufnr, ns_id, linenr_start = 0, -1, 1 + +line:render(bufnr, ns_id, linenr_start) +``` + +## Parameters + +### `texts` + +**Type:** `table[]` + +List of `NuiText` objects to set as initial texts. + +**Example** + +```lua +local text_one = NuiText("One") +local text_two = NuiText("Two") +local line = NuiLine({ text_one, text_two }) +``` + +## Methods + +### `line:append` + +_Signature:_ `line:append(content, highlight?)` + +Adds a chunk of content to the line. + +**Parameters** + +| Name | Type | Description | +| ----------- | -------------------------------- | --------------------- | +| `content` | `string` / `NuiText` / `NuiLine` | content | +| `highlight` | `string` or `table` | highlight information | + +If `text` is `string`, these parameters are passed to `NuiText` +and a `NuiText` object is returned. + +It `content` is a `NuiText`/`NuiLine` object, it is returned unchanged. + +### `line:content` + +_Signature:_ `line:content()` + +Returns the line content. + +### `line:highlight` + +_Signature:_ `line:highlight(bufnr, ns_id, linenr)` + +Applies highlight for the line. + +**Parameters** + +| Name | Type | Description | +| -------- | -------- | ---------------------------------------------- | +| `bufnr` | `number` | buffer number | +| `ns_id` | `number` | namespace id (use `-1` for fallback namespace) | +| `linenr` | `number` | line number (1-indexed) | + +### `line:render` + +_Signature:_ `line:render(bufnr, ns_id, linenr_start, linenr_end?)` + +Sets the line on buffer and applies highlight. + +**Parameters** + +| Name | Type | Description | +| -------------- | -------- | ---------------------------------------------- | +| `bufnr` | `number` | buffer number | +| `ns_id` | `number` | namespace id (use `-1` for fallback namespace) | +| `linenr_start` | `number` | start line number (1-indexed) | +| `linenr_end` | `number` | end line number (1-indexed) | + +## Wiki Page + +You can find additional documentation/examples/guides/tips-n-tricks in [nui.line wiki page](https://github.com/MunifTanjim/nui.nvim/wiki/nui.line). diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/line/init.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/line/init.lua new file mode 100644 index 0000000..14aa713 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/line/init.lua @@ -0,0 +1,80 @@ +local Object = require("nui.object") +local NuiText = require("nui.text") +local defaults = require("nui.utils").defaults + +---@class NuiLine +---@field _texts NuiText[] +local Line = Object("NuiLine") + +---@param texts? NuiText[] +function Line:init(texts) + self._texts = defaults(texts, {}) +end + +---@param content string|NuiText|NuiLine +---@param highlight? string|nui_text_extmark data for highlight +---@return NuiText|NuiLine +function Line:append(content, highlight) + local block = content + if type(block) == "string" then + block = NuiText(block, highlight) + end + if block._texts then + ---@cast block NuiLine + for _, text in ipairs(block._texts) do + table.insert(self._texts, text) + end + else + ---@cast block NuiText + table.insert(self._texts, block) + end + return block +end + +---@return string +function Line:content() + return table.concat(vim.tbl_map(function(text) + return text:content() + end, self._texts)) +end + +---@return number +function Line:width() + local width = 0 + for _, text in ipairs(self._texts) do + width = width + text:width() + end + return width +end + +---@param bufnr number buffer number +---@param ns_id number namespace id +---@param linenr number line number (1-indexed) +---@param ___byte_start___? integer start byte position (0-indexed) +---@return nil +function Line:highlight(bufnr, ns_id, linenr, ___byte_start___) + local current_byte_start = ___byte_start___ or 0 + for _, text in ipairs(self._texts) do + text:highlight(bufnr, ns_id, linenr, current_byte_start) + current_byte_start = current_byte_start + text:length() + end +end + +---@param bufnr number buffer number +---@param ns_id number namespace id +---@param linenr_start number start line number (1-indexed) +---@param linenr_end? number end line number (1-indexed) +---@return nil +function Line:render(bufnr, ns_id, linenr_start, linenr_end) + local row_start = linenr_start - 1 + local row_end = linenr_end and linenr_end - 1 or row_start + 1 + local content = self:content() + vim.api.nvim_buf_set_lines(bufnr, row_start, row_end, false, { content }) + self:highlight(bufnr, ns_id, linenr_start) +end + +---@alias NuiLine.constructor fun(texts?: NuiText[]): NuiLine +---@type NuiLine|NuiLine.constructor +local NuiLine = Line + +return NuiLine diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/menu/README.md b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/menu/README.md new file mode 100644 index 0000000..6267d8b --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/menu/README.md @@ -0,0 +1,207 @@ +# Menu + +`Menu` is abstraction layer on top of `Popup`. + +```lua +local Menu = require("nui.menu") +local event = require("nui.utils.autocmd").event + +local popup_options = { + relative = "cursor", + position = { + row = 1, + col = 0, + }, + border = { + style = "rounded", + text = { + top = "[Choose Item]", + top_align = "center", + }, + }, + win_options = { + winhighlight = "Normal:Normal", + } +} + +local menu = Menu(popup_options, { + lines = { + Menu.separator("Group One"), + Menu.item("Item 1"), + Menu.item("Item 2"), + Menu.separator("Group Two", { + char = "-", + text_align = "right", + }), + Menu.item("Item 3"), + Menu.item("Item 4"), + }, + max_width = 20, + keymap = { + focus_next = { "j", "", "" }, + focus_prev = { "k", "", "" }, + close = { "", "" }, + submit = { "", "" }, + }, + on_close = function() + print("CLOSED") + end, + on_submit = function(item) + print("SUBMITTED", vim.inspect(item)) + end, +}) +``` + +You can manipulate the associated buffer and window using the +`split.bufnr` and `split.winid` properties. + +**NOTE**: the first argument accepts options for `nui.popup` component. + +## Options + +### `lines` + +**Type:** `table` + +List of menu items. + +**`Menu.item(content, data?)`** + +`Menu.item` is used to create an item object for the `Menu`. + +**Parameters** + +| Name | Type | +| --------- | -------------------------------- | +| `content` | `string` / `NuiText` / `NuiLine` | +| `data` | `table` / `nil` | + +**Example** + +```lua +Menu.item("One") --> { text = "One" } + +Menu.item("Two", { id = 2 }) --> { id = 2, text = "Two" } +``` + +This is what you get as the argument of `on_submit` callback function. +You can include whatever you want in the item object. + +**`Menu.separator(content?, options?)`** + +`Menu.separator` is used to create a menu item that can't be focused. + +**Parameters** + +| Name | Type | +| --------- | ---------------------------------------------------------------------------------- | +| `content` | `string` / `NuiText` / `NuiLine` / `nil` | +| `options` | `{ char?: string\|NuiText, text_align?: "'left'"\|"'center'"\|"'right'" }` / `nil` | + +You can just use `Menu.item` only and implement `Menu.separator`'s behavior +by providing a custom `should_skip_item` function. + +### `prepare_item` + +**Type:** `function` + +_Signature:_ `prepare_item(item)` + +If provided, this function is used for preparing each menu item. + +The return value should be a `NuiLine` object or `string` or a list containing either of them. + +If return value is `nil`, that node will not be rendered. + +### `should_skip_item` + +**Type:** `function` + +_Signature:_ `should_skip_item(item)` + +If provided, this function is used to determine if an item should be +skipped when focusing previous/next item. + +The return value should be `boolean`. + +By default, items created by `Menu.separator` are skipped. + +### `max_height` + +**Type:** `number` + +Maximum height of the menu. + +### `min_height` + +**Type:** `number` + +Minimum height of the menu. + +### `max_width` + +**Type:** `number` + +Maximum width of the menu. + +### `min_width` + +**Type:** `number` + +Minimum width of the menu. + +### `keymap` + +**Type:** `table` + +Key mappings for the menu. + +**Example** + +```lua +keymap = { + close = { "", "" }, + focus_next = { "j", "", "" }, + focus_prev = { "k", "", "" }, + submit = { "" }, +}, +``` + +### `on_change` + +**Type:** `function` + +_Signature:_ `on_change(item, menu) -> nil` + +Callback function, called when menu item is focused. + +### `on_close` + +**Type:** `function` + +_Signature:_ `on_close() -> nil` + +Callback function, called when menu is closed. + +### `on_submit` + +**Type:** `function` + +_Signature:_ `on_submit(item) -> nil` + +Callback function, called when menu is submitted. + +## Methods + +Methods from `nui.popup` are also available for `nui.menu`. + +## Properties + +### `menu.tree` + +The underlying `NuiTree` object used for rendering the menu. You can use it to +manipulate the menu items on-the-fly and access all the `NuiTree` methods. + +## Wiki Page + +You can find additional documentation/examples/guides/tips-n-tricks in [nui.menu wiki page](https://github.com/MunifTanjim/nui.nvim/wiki/nui.menu). diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/menu/init.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/menu/init.lua new file mode 100644 index 0000000..28554bb --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/menu/init.lua @@ -0,0 +1,377 @@ +local Line = require("nui.line") +local Popup = require("nui.popup") +local Text = require("nui.text") +local Tree = require("nui.tree") +local _ = require("nui.utils")._ +local defaults = require("nui.utils").defaults +local is_type = require("nui.utils").is_type + +local function calculate_initial_max_width(items) + local max_width = 0 + + for _, item in ipairs(items) do + local width = 0 + if is_type("string", item.text) then + width = vim.api.nvim_strwidth(item.text) + elseif is_type("table", item.text) and item.text.width then + width = item.text:width() + end + + if max_width < width then + max_width = width + end + end + + return max_width +end + +local default_keymap = { + close = { "", "" }, + focus_next = { "j", "", "" }, + focus_prev = { "k", "", "" }, + submit = { "" }, +} + +---@param keymap table<_nui_menu_keymap_action, string|string[]> +---@return table<_nui_menu_keymap_action, string[]> +local function parse_keymap(keymap) + local result = defaults(keymap, {}) + + for name, default_keys in pairs(default_keymap) do + if is_type("nil", result[name]) then + result[name] = default_keys + elseif is_type("string", result[name]) then + result[name] = { result[name] } + end + end + + return result +end + +---@type nui_menu_should_skip_item +local function default_should_skip_item(node) + return node._type == "separator" +end + +---@param menu NuiMenu +---@return nui_menu_prepare_item +local function make_default_prepare_node(menu) + local border = menu.border + + local fallback_sep = { + char = Text(is_type("table", border._.char) and border._.char.top or " "), + text_align = is_type("table", border._.text) and border._.text.top_align or "left", + } + + -- luacov: disable + if menu._.sep then + -- @deprecated + + if menu._.sep.char then + fallback_sep.char = Text(menu._.sep.char) + end + + if menu._.sep.text_align then + fallback_sep.text_align = menu._.sep.text_align + end + end + -- luacov: enable + + local max_width = menu._.size.width + + ---@type nui_menu_prepare_item + local function default_prepare_node(node) + ---@type NuiText|NuiLine + local content = is_type("string", node.text) and Text(node.text) or node.text + + if node._type == "item" then + if content:width() > max_width then + if is_type("function", content.set) then + ---@cast content NuiText + _.truncate_nui_text(content, max_width) + else + ---@cast content NuiLine + _.truncate_nui_line(content, max_width) + end + end + + local line = Line() + + line:append(content) + + return line + end + + if node._type == "separator" then + local sep_char = Text(defaults(node._char, fallback_sep.char)) + local sep_text_align = defaults(node._text_align, fallback_sep.text_align) + + local sep_max_width = max_width - sep_char:width() * 2 + + if content:width() > sep_max_width then + if content._texts then + ---@cast content NuiLine + _.truncate_nui_line(content, sep_max_width) + else + ---@cast content NuiText + _.truncate_nui_text(content, sep_max_width) + end + end + + local left_gap_width, right_gap_width = + _.calculate_gap_width(defaults(sep_text_align, "center"), sep_max_width, content:width()) + + local line = Line() + + line:append(Text(sep_char)) + + if left_gap_width > 0 then + line:append(Text(sep_char):set(string.rep(sep_char:content(), left_gap_width))) + end + + line:append(content) + + if right_gap_width > 0 then + line:append(Text(sep_char):set(string.rep(sep_char:content(), right_gap_width))) + end + + line:append(Text(sep_char)) + + return line + end + end + + return default_prepare_node +end + +---@param menu NuiMenu +---@param direction "'next'" | "'prev'" +---@param current_linenr nil | number +local function focus_item(menu, direction, current_linenr) + local curr_linenr = current_linenr or vim.api.nvim_win_get_cursor(menu.winid)[1] + + local next_linenr = nil + + if direction == "next" then + if curr_linenr == #menu.tree:get_nodes() then + next_linenr = 1 + else + next_linenr = curr_linenr + 1 + end + elseif direction == "prev" then + if curr_linenr == 1 then + next_linenr = #menu.tree:get_nodes() + else + next_linenr = curr_linenr - 1 + end + end + + local next_node = menu.tree:get_node(next_linenr) + + if menu._.should_skip_item(next_node) then + return focus_item(menu, direction, next_linenr) + end + + if next_linenr then + vim.api.nvim_win_set_cursor(menu.winid, { next_linenr, 0 }) + menu._.on_change(next_node) + end +end + +---@alias nui_menu_prepare_item nui_tree_prepare_node +---@alias nui_menu_should_skip_item fun(node: NuiTree.Node): boolean + +---@alias _nui_menu_keymap_action 'close'|'focus_next'|'focus_prev'|'submit' + +---@class nui_menu_internal: nui_popup_internal +---@field items NuiTree.Node[] +---@field keymap table<_nui_menu_keymap_action, string[]> +---@field sep { char?: string|NuiText, text_align?: nui_t_text_align } # deprecated +---@field prepare_item nui_menu_prepare_item +---@field should_skip_item nui_menu_should_skip_item +---@field on_change fun(item: NuiTree.Node): nil + +---@class nui_menu_options +---@field lines NuiTree.Node[] +---@field prepare_item? nui_tree_prepare_node +---@field should_skip_item? nui_menu_should_skip_item +---@field max_height? integer +---@field min_height? integer +---@field max_width? integer +---@field min_width? integer +---@field keymap? table<_nui_menu_keymap_action, string|string[]> +---@field on_change? fun(item: NuiTree.Node, menu: NuiMenu): nil +---@field on_close? fun(): nil +---@field on_submit? fun(item: NuiTree.Node): nil + +---@class NuiMenu: NuiPopup +---@field private _ nui_menu_internal +local Menu = Popup:extend("NuiMenu") + +---@param content? string|NuiText|NuiLine +---@param options? { char?: string|NuiText, text_align?: nui_t_text_align } +---@return NuiTree.Node +function Menu.separator(content, options) + options = options or {} + return Tree.Node({ + _id = tostring(math.random()), + _type = "separator", + _char = options.char, + _text_align = options.text_align, + text = defaults(content, ""), + }) +end + +---@param content string|NuiText|NuiLine +---@param data? table +---@return NuiTree.Node +function Menu.item(content, data) + if not data then + ---@diagnostic disable-next-line: undefined-field + if is_type("table", content) and content.text then + ---@cast content table + data = content + else + data = { text = content } + end + else + data.text = content + end + + data._type = "item" + data._id = data.id or tostring(math.random()) + + return Tree.Node(data) +end + +---@param popup_options nui_popup_options +---@param options nui_menu_options +function Menu:init(popup_options, options) + local max_width = calculate_initial_max_width(options.lines) + + local width = math.max(math.min(max_width, defaults(options.max_width, 256)), defaults(options.min_width, 4)) + local height = math.max(math.min(#options.lines, defaults(options.max_height, 256)), defaults(options.min_height, 1)) + + ---@type nui_popup_options + popup_options = vim.tbl_deep_extend("force", { + enter = true, + size = { + width = width, + height = height, + }, + win_options = { + cursorline = true, + scrolloff = 1, + sidescrolloff = 0, + }, + zindex = 60, + }, popup_options) + + Menu.super.init(self, popup_options) + + self._.items = options.lines + self._.keymap = parse_keymap(options.keymap) + + ---@param node NuiTree.Node + self._.on_change = function(node) + if options.on_change then + options.on_change(node, self) + end + end + + ---@deprecated + self._.sep = options.separator + + self._.should_skip_item = defaults(options.should_skip_item, default_should_skip_item) + self._.prepare_item = defaults(options.prepare_item, self._.prepare_item) + + self.menu_props = {} + + local props = self.menu_props + + props.on_submit = function() + local item = self.tree:get_node() + + self:unmount() + + if options.on_submit then + options.on_submit(item) + end + end + + props.on_close = function() + self:unmount() + + if options.on_close then + options.on_close() + end + end + + props.on_focus_next = function() + focus_item(self, "next") + end + + props.on_focus_prev = function() + focus_item(self, "prev") + end +end + +---@param config? nui_layout_options +function Menu:update_layout(config) + Menu.super.update_layout(self, config) + + self._.prepare_item = defaults(self._.prepare_item, make_default_prepare_node(self)) +end + +function Menu:mount() + Menu.super.mount(self) + + local props = self.menu_props + + for _, key in pairs(self._.keymap.focus_next) do + self:map("n", key, props.on_focus_next, { noremap = true, nowait = true }) + end + + for _, key in pairs(self._.keymap.focus_prev) do + self:map("n", key, props.on_focus_prev, { noremap = true, nowait = true }) + end + + for _, key in pairs(self._.keymap.close) do + self:map("n", key, props.on_close, { noremap = true, nowait = true }) + end + + for _, key in pairs(self._.keymap.submit) do + self:map("n", key, props.on_submit, { noremap = true, nowait = true }) + end + + self.tree = Tree({ + winid = self.winid, + ns_id = self.ns_id, + nodes = self._.items, + get_node_id = function(node) + return node._id + end, + prepare_node = self._.prepare_item, + }) + + ---@deprecated + self._tree = self.tree + + self.tree:render() + + -- focus first item + for linenr = 1, #self.tree:get_nodes() do + local node, target_linenr = self.tree:get_node(linenr) + if not self._.should_skip_item(node) then + vim.api.nvim_win_set_cursor(self.winid, { target_linenr, 0 }) + self._.on_change(node) + break + end + end +end + +---@alias NuiMenu.constructor fun(popup_options: nui_popup_options, options: nui_menu_options): NuiMenu +---@type NuiMenu|NuiMenu.constructor +local NuiMenu = Menu + +return NuiMenu diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/object/init.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/object/init.lua new file mode 100644 index 0000000..716a2e4 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/object/init.lua @@ -0,0 +1,170 @@ +-- source: https://github.com/kikito/middleclass + +local idx = { + subclasses = { "" }, +} + +local function __tostring(self) + return "class " .. self.name +end + +local function __call(self, ...) + return self:new(...) +end + +local function create_index_wrapper(class, index) + if type(index) == "table" then + return function(self, key) + local value = self.class.__meta[key] + if value == nil then + return index[key] + end + return value + end + elseif type(index) == "function" then + return function(self, key) + local value = self.class.__meta[key] + if value == nil then + return index(self, key) + end + return value + end + else + return class.__meta + end +end + +local function propagate_instance_property(class, key, value) + value = key == "__index" and create_index_wrapper(class, value) or value + + class.__meta[key] = value + + for subclass in pairs(class[idx.subclasses]) do + if subclass.__properties[key] == nil then + propagate_instance_property(subclass, key, value) + end + end +end + +local function declare_instance_property(class, key, value) + class.__properties[key] = value + + if value == nil and class.super then + value = class.super.__meta[key] + end + + propagate_instance_property(class, key, value) +end + +local function is_subclass(subclass, class) + if not subclass.super then + return false + end + if subclass.super == class then + return true + end + return is_subclass(subclass.super, class) +end + +local function is_instance(instance, class) + if instance.class == class then + return true + end + return is_subclass(instance.class, class) +end + +local function create_class(name, super) + assert(name, "missing name") + + local meta = { + is_instance_of = is_instance, + } + meta.__index = meta + + local class = { + super = super, + name = name, + static = { + is_subclass_of = is_subclass, + }, + + [idx.subclasses] = setmetatable({}, { __mode = "k" }), + + __meta = meta, + __properties = {}, + } + + setmetatable(class.static, { + __index = function(_, key) + local value = rawget(class.__meta, key) + if value == nil and super then + return super.static[key] + end + return value + end, + }) + + setmetatable(class, { + __call = __call, + __index = class.static, + __name = class.name, + __newindex = declare_instance_property, + __tostring = __tostring, + }) + + return class +end + +---@param name string +local function create_object(_, name) + local Class = create_class(name) + + ---@return string + function Class:__tostring() + return "instance of " .. tostring(self.class) + end + + ---@return nil + function Class:init() end -- luacheck: no unused args + + function Class.static:new(...) + local instance = setmetatable({ class = self }, self.__meta) + instance:init(...) + return instance + end + + ---@param name string + function Class.static:extend(name) -- luacheck: no redefined + local subclass = create_class(name, self) + + for key, value in pairs(self.__meta) do + if not (key == "__index" and type(value) == "table") then + propagate_instance_property(subclass, key, value) + end + end + + function subclass.init(instance, ...) + self.init(instance, ...) + end + + self[idx.subclasses][subclass] = true + + return subclass + end + + return Class +end + +--luacheck: push no max line length + +---@type (fun(name: string): table)|{ is_subclass: (fun(subclass: table, class: table): boolean), is_instance: (fun(instance: table, class: table): boolean) } +local Object = setmetatable({ + is_subclass = is_subclass, + is_instance = is_instance, +}, { + __call = create_object, +}) + +--luacheck: pop + +return Object diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/popup/README.md b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/popup/README.md new file mode 100644 index 0000000..a8a7545 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/popup/README.md @@ -0,0 +1,674 @@ +# Popup + +Popup is an abstraction layer on top of window. + +Creates a new popup object (but does not mount it immediately). + +**Examples** + +```lua +local Popup = require("nui.popup") + +local popup = Popup({ + position = "50%", + size = { + width = 80, + height = 40, + }, + enter = true, + focusable = true, + zindex = 50, + relative = "editor", + border = { + padding = { + top = 2, + bottom = 2, + left = 3, + right = 3, + }, + style = "rounded", + text = { + top = " I am top title ", + top_align = "center", + bottom = "I am bottom title", + bottom_align = "left", + }, + }, + buf_options = { + modifiable = true, + readonly = false, + }, + win_options = { + winblend = 10, + winhighlight = "Normal:Normal,FloatBorder:FloatBorder", + }, +}) +``` + +You can manipulate the associated buffer and window using the +`split.bufnr` and `split.winid` properties. + +## Options + +### `border` + +**Type:** `table` + +Contains all border related options. + +#### `border.padding` + +**Type:** `table` + +Controls the popup padding. + +**Examples** + +It can be a list (`table`) with number of cells for top, right, bottom and left. +The order behaves like [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS) padding. + +```lua +border = { + -- `1` for top/bottom and `2` for left/right + padding = { 1, 2 }, +}, +``` + +You can also use a map (`table`) to set padding at specific side: + +```lua +border = { + -- `1` for top, `2` for left, `0` for other sides + padding = { + top = 1, + left = 2, + }, +}, +``` + +#### `border.style` + +**Type:** `string` or `table` + +Controls the styling of the border. + +**Examples** + +Can be one of the pre-defined styles: `"double"`, `"none"`, `"rounded"`, `"shadow"`, `"single"`, `"solid"` or `"default"`. + +```lua +border = { + style = "double", +}, +``` + +List (`table`) of characters starting from the top-left corner and then clockwise: + +```lua +border = { + style = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" }, +}, +``` + +Map (`table`) with named characters: + +```lua +border = { + style = { + top_left = "╭", top = "─", top_right = "╮", + left = "│", right = "│", + bottom_left = "╰", bottom = "─", bottom_right = "╯", + }, +}, +``` + +If you don't need all these options, you can also pass the value of `border.style` to `border` +directly. + +To set the highlight group for all the border characters, use the `win_options.winhighlight` +option and include the name of highlight group for `FloatBorder`. + +**Examples** + +```lua +win_options = { + winhighlight = "Normal:Normal,FloatBorder:SpecialChar", +}, +``` + +To set the highlight group for individual border character, you can use `NuiText` or a tuple +with `(char, hl_group)`. + +**Examples** + +```lua +border = { + style = { { [[/]], "SpecialChar" }, [[─]], NuiText([[\]], "SpecialChar"), [[│]] }, +}, +``` + +#### `border.text` + +**Type:** `table` + +Text displayed on the border (as title/footnote). + +| Key | Type | Description | +| ---------------- | -------------------------------------------- | ---------------------------- | +| `"top"` | `string` / `NuiLine` / `NuiText` | top border text | +| `"top_align"` | `"left"` / `"right"`/ `"center"` _(default)_ | top border text alignment | +| `"bottom"` | `string` / `NuiLine` / `NuiText` | bottom border text | +| `"bottom_align"` | `"left"` / `"right"`/ `"center"` _(default)_ | bottom border text alignment | + +`"top"` and `"bottom"` also supports list of `(text, hl_group)` tuples, just like the native popup. + +**Examples** + +```lua +border = { + text = { + top = "Popup Title", + top_align = "center", + }, +}, +``` + +--- + +### `ns_id` + +**Type:** `number` or `string` + +Namespace id (`number`) or name (`string`). + +--- + +### `anchor` + +**Type:** `"NW"` / `"NE"` / `"SW"` / `"SE"` + +Decides which corner of the popup to place at `position`. + +--- + +### `relative` + +**Type:** `string` or `table` + +This option affects how `position` and `size` are calculated. + +**Examples** + +Relative to cursor on current window: + +```lua +relative = "cursor", +``` + +Relative to the current editor screen: + +```lua +relative = "editor", +``` + +Relative to the current window (_default_): + +```lua +relative = "win", +``` + +Relative to the window with specific id: + +```lua +relative = { + type = "win", + winid = 5, +}, +``` + +Relative to the buffer position: + +```lua +relative = { + type = "buf", + -- zero-indexed + position = { + row = 5, + col = 5, + }, +}, +``` + +--- + +### `position` + +**Type:** `number` or `percentage string` or `table` + +Position is calculated from the top-left corner. + +If `position` is `number` or `percentage string`, it applies to both `row` and `col`. +Or you can pass a table to set them separately. + +For `percentage string`, position is calculated according to the option `relative`. +If `relative` is set to `"buf"` or `"cursor"`, `percentage string` is not allowed. + +**Examples** + +```lua +position = 50, +``` + +```lua +position = "50%", +``` + +```lua +position = { + row = 30, + col = 20, +}, +``` + +```lua +position = { + row = "20%", + col = "50%", +}, +``` + +--- + +### `size` + +**Type:** `number` or `percentage string` or `table` + +Determines the size of the popup. + +If `size` is `number` or `percentage string`, it applies to both `width` and `height`. +You can also pass a table to set them separately. + +For `percentage string`, `size` is calculated according to the option `relative`. +If `relative` is set to `"buf"` or `"cursor"`, window size is considered. + +Decimal `number` in `(0,1)` range is treated similar to `percentage string`. For +example: `0.5` is same as `"50%"`. + +**Examples** + +```lua +size = 50, +``` + +```lua +size = "50%", +``` + +```lua +size = 0.5, +``` + +```lua +size = { + width = 80, + height = 40, +}, +``` + +```lua +size = { + width = "80%", + height = 0.6, +}, +``` + +--- + +### `enter` + +**Type:** `boolean` + +If `true`, the popup is entered immediately after mount. + +**Examples** + +```lua +enter = true, +``` + +--- + +### `focusable` + +**Type:** `boolean` + +If `false`, the popup can not be entered by user actions (wincmds, mouse events). + +**Examples** + +```lua +focusable = true, +``` + +--- + +### `zindex` + +**Type:** `number` + +Sets the order of the popup on z-axis. + +Popup with higher the `zindex` goes on top of popups with lower `zindex`. + +**Examples** + +```lua +zindex = 50, +``` + +--- + +### `buf_options` + +**Type:** `table` + +Contains all buffer related options (check `:h options | /local to buffer`). + +**Examples** + +```lua +buf_options = { + modifiable = false, + readonly = true, +}, +``` + +--- + +### `win_options` + +**Type:** `table` + +Contains all window related options (check `:h options | /local to window`). + +**Examples** + +```lua +win_options = { + winblend = 10, + winhighlight = "Normal:Normal,FloatBorder:FloatBorder", +}, +``` + +--- + +### `bufnr` + +**Type:** `number` + +You can pass `bufnr` of an existing buffer to display it on the popup. + +**Examples:** + +```lua +bufnr = vim.api.nvim_get_current_buf(), +``` + +## Methods + +### `popup:mount` + +_Signature:_ `popup:mount()` + +Mounts the popup. + +**Examples** + +```lua +popup:mount() +``` + +--- + +### `popup:unmount` + +_Signature:_ `popup:unmount()` + +Unmounts the popup. + +**Examples** + +```lua +popup:unmount() +``` + +--- + +### `popup:hide` + +_Signature:_ `popup:hide()` + +Hides the popup window. Preserves the buffer (related content, autocmds and keymaps). + +--- + +### `popup:show` + +_Signature:_ `popup:show()` + +Shows the hidden popup window. + +--- + +### `popup:map` + +_Signature:_ `popup:map(mode, key, handler, opts) -> nil` + +Sets keymap for the popup. + +**Parameters** + +| Name | Type | Description | +| --------- | --------------------- | --------------------------------------------------------------------------- | +| `mode` | `string` | check `:h :map-modes` | +| `key` | `string` | key for the mapping | +| `handler` | `string` / `function` | handler for the mapping | +| `opts` | `table` | check `:h :map-arguments` (including `remap`/`noremap`, excluding `buffer`) | + +**Examples** + +```lua +local ok = popup:map("n", "", function(bufnr) + print("ESC pressed in Normal mode!") +end, { noremap = true }) +``` + +--- + +### `popup:unmap` + +_Signature:_ `popup:unmap(mode, key) -> nil` + +Deletes keymap for the popup. + +**Parameters** + +| Name | Type | Description | +| ------ | ------------- | --------------------- | +| `mode` | `"n"` / `"i"` | check `:h :map-modes` | +| `key` | `string` | key for the mapping | + +**Examples** + +```lua +local ok = popup:unmap("n", "") +``` + +--- + +### `popup:on` + +_Signature:_ `popup:on(event, handler, options)` + +Defines `autocmd` to run on specific events for this popup. + +**Parameters** + +| Name | Type | Description | +| --------- | --------------------- | ------------------------------------------ | +| `event` | `string[]` / `string` | check `:h events` | +| `handler` | `function` | handler function for event | +| `options` | `table` | keys `once`, `nested` and values `boolean` | + +**Examples** + +```lua +local event = require("nui.utils.autocmd").event + +popup:on({ event.BufLeave }, function() + popup:unmount() +end, { once = true }) +``` + +`event` can be expressed as any of the followings: + +```lua +{ event.BufLeave, event.BufDelete } +-- or +{ event.BufLeave, "BufDelete" } +-- or +event.BufLeave +-- or +"BufLeave" +-- or +"BufLeave,BufDelete" +``` + +--- + +### `popup:off` + +_Signature:_ `popup:off(event)` + +Removes `autocmd` defined with `popup:on({ ... })` + +**Parameters** + +| Name | Type | Description | +| ------- | --------------------- | ----------------- | +| `event` | `string[]` / `string` | check `:h events` | + +**Examples** + +```lua +popup:off("*") +``` + +--- + +### `popup:update_layout` + +_Signature:_ `popup:update_layout(config)` + +Sets the layout of the popup. You can use this method to change popup's +size or position after it's mounted. + +**Parameters** + +`config` is a `table` having the following keys: + +| Key | Type | +| ---------- | --------------------------------- | +| `anchor` | `"NW"` / `"NE"` / `"SW"` / `"SE"` | +| `relative` | `string` / `table` | +| `position` | `string` / `table` | +| `size` | `string` / `table` | + +They are the same options used for popup initialization. + +**Examples** + +```lua +popup:update_layout({ + relative = "win", + size = { + width = 80, + height = 40, + }, + position = { + row = 30, + col = 20, + }, +}) +``` + +--- + +### `popup.border:set_highlight` + +_Signature:_ `popup.border:set_highlight(highlight: string) -> nil` + +Sets border highlight. + +**Parameters** + +| Name | Type | Description | +| ----------- | -------- | -------------------- | +| `highlight` | `string` | highlight group name | + +**Examples** + +```lua +popup.border:set_highlight("SpecialChar") +``` + +--- + +### `popup.border:set_style` + +_Signature:_ `popup.border:set_style(style: string|table) -> nil` + +Sets border style. + +**Parameters** + +| Name | Type | Description | +| ------- | ------------------ | ------------ | +| `style` | `string` / `table` | border style | + +This `style` parameter is exactly the same as popup option `border.style`. + +You'll need to call `popup:update_layout()` after this for the change to render on screen. + +**Examples** + +```lua +popup.border:set_style("rounded") +popup:update_layout() +``` + +--- + +### `popup.border:set_text` + +_Signature:_ `popup.border:set_text(edge, text, align)` + +Sets border text. + +**Parameters** + +| Name | Type | +| ------- | ------------------------------------------- | +| `edge` | `"top"` / `"bottom"` / `"left"` / `"right"` | +| `text` | `string` | +| `align` | `"left"` / `"right"`/ `"center"` | + +**Examples** + +```lua +popup.border:set_text("bottom", "[Progress: 42%]", "right") +``` + +## Wiki Page + +You can find additional documentation/examples/guides/tips-n-tricks in [nui.popup wiki page](https://github.com/MunifTanjim/nui.nvim/wiki/nui.popup). diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/popup/border.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/popup/border.lua new file mode 100644 index 0000000..9974922 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/popup/border.lua @@ -0,0 +1,746 @@ +---@diagnostic disable: invisible + +local Object = require("nui.object") +local Line = require("nui.line") +local Text = require("nui.text") +local _ = require("nui.utils")._ +local is_type = require("nui.utils").is_type + +local has_nvim_0_5_1 = vim.fn.has("nvim-0.5.1") == 1 +local has_nvim_0_11_0 = _.feature.v0_11 + +local index_name = { + "top_left", + "top", + "top_right", + "right", + "bottom_right", + "bottom", + "bottom_left", + "left", +} + +---@param border _nui_popup_border_style_list +---@return _nui_popup_border_style_map +local function to_border_map(border) + local count = vim.tbl_count(border) --[[@as integer]] + if count < 8 then + -- fillup all 8 characters + for i = count + 1, 8 do + local fallback_index = i % count + local char = border[fallback_index == 0 and count or fallback_index] + if type(char) == "table" then + char = char.content and Text(char) or vim.deepcopy(char) + end + border[i] = char + end + end + + ---@type _nui_popup_border_style_map + local named_border = {} + for index, name in ipairs(index_name) do + named_border[name] = border[index] + end + return named_border +end + +---@param char _nui_popup_border_style_map +---@return _nui_popup_border_internal_char +local function normalize_char_map(char) + if not char or type(char) == "string" then + return char + end + + for position, item in pairs(char) do + if type(item) == "string" then + char[position] = Text(item, "FloatBorder") + elseif not item.content then + char[position] = Text(item[1], item[2] or "FloatBorder") + elseif item.extmark then + item.extmark.hl_group = item.extmark.hl_group or "FloatBorder" + else + item.extmark = { hl_group = "FloatBorder" } + end + end + + return char --[[@as _nui_popup_border_internal_char]] +end + +---@param char? NuiText +---@return boolean +local function is_empty_char(char) + return not char or 0 == char:width() +end + +---@param text? _nui_popup_border_option_text_value +---@return nil|NuiLine|NuiText +local function normalize_border_text(text) + if not text then + return text + end + + if type(text) == "string" then + return Text(text, "FloatTitle") + end + + if text.content then + for _, text_chunk in ipairs(text._texts or { text }) do + text_chunk.extmark = vim.tbl_deep_extend("keep", text_chunk.extmark or {}, { + hl_group = "FloatTitle", + }) + end + return text --[[@as NuiLine|NuiText]] + end + + local line = Line() + for _, chunk in ipairs(text) do + if type(chunk) == "string" then + line:append(chunk, "FloatTitle") + else + line:append(chunk[1], chunk[2] or "FloatTitle") + end + end + return line +end + +---@param internal nui_popup_border_internal +---@param popup_winhighlight? string +---@return nil|string +local function calculate_winhighlight(internal, popup_winhighlight) + if internal.type == "simple" then + return + end + + local winhl = popup_winhighlight + + -- @deprecated + if internal.highlight then + if not string.match(internal.highlight, ":") then + internal.highlight = "FloatBorder:" .. internal.highlight + end + + winhl = internal.highlight + internal.highlight = nil + end + + return winhl +end + +---@param padding? nui_popup_border_option_padding +---@return nil|nui_popup_border_internal_padding +local function normalize_option_padding(padding) + if not padding then + return nil + end + + if is_type("map", padding) then + ---@cast padding _nui_popup_border_option_padding_map + return padding + end + + local map = {} + + ---@cast padding _nui_popup_border_option_padding_list + map.top = padding[1] or 0 + map.right = padding[2] or map.top + map.bottom = padding[3] or map.top + map.left = padding[4] or map.right + + return map +end + +---@param text? nui_popup_border_option_text +---@return nil|nui_popup_border_internal_text +local function normalize_option_text(text) + if not text then + return text + end + + text.top = normalize_border_text(text.top) + text.bottom = normalize_border_text(text.bottom) + + return text --[[@as nui_popup_border_internal_text]] +end + +---@param edge 'top'|'bottom' +---@param text? NuiLine|NuiText +---@param align? nui_t_text_align +---@return NuiLine +local function calculate_buf_edge_line(internal, edge, text, align) + local char, size = internal.char, internal.size + + local left_char = char[edge .. "_left"] + local mid_char = char[edge] + local right_char = char[edge .. "_right"] + + if left_char:content() == "" then + left_char = Text(mid_char:content() == "" and char["left"] or mid_char) + end + + if right_char:content() == "" then + right_char = Text(mid_char:content() == "" and char["right"] or mid_char) + end + + local max_width = size.width - left_char:width() - right_char:width() + + local content = Line() + if mid_char:width() == 0 then + content:append(string.rep(" ", max_width)) + else + content:append(text or "") + end + + _.truncate_nui_line(content, max_width) + + local left_gap_width, right_gap_width = _.calculate_gap_width(align or "center", max_width, content:width()) + + local line = Line() + + line:append(left_char) + + if left_gap_width > 0 then + line:append(Text(mid_char):set(string.rep(mid_char:content(), left_gap_width))) + end + + line:append(content) + + if right_gap_width > 0 then + line:append(Text(mid_char):set(string.rep(mid_char:content(), right_gap_width))) + end + + line:append(right_char) + + return line +end + +---@param internal nui_popup_border_internal +---@return nil|NuiLine[] +local function calculate_buf_lines(internal) + local char, size, text = internal.char, internal.size, internal.text or {} + + if type(char) == "string" then + return nil + end + + local left_char, right_char = char.left, char.right + + local gap_length = size.width - left_char:width() - right_char:width() + + ---@type NuiLine[] + local lines = {} + + table.insert(lines, calculate_buf_edge_line(internal, "top", text.top, text.top_align)) + for _ = 1, size.height - 2 do + table.insert( + lines, + Line({ + Text(left_char), + Text(string.rep(" ", gap_length)), + Text(right_char), + }) + ) + end + table.insert(lines, calculate_buf_edge_line(internal, "bottom", text.bottom, text.bottom_align)) + + return lines +end + +local styles = { + bold = to_border_map({ "┏", "━", "┓", "┃", "┛", "━", "┗", "┃" }), + double = to_border_map({ "╔", "═", "╗", "║", "╝", "═", "╚", "║" }), + none = "none", + rounded = to_border_map({ "╭", "─", "╮", "│", "╯", "─", "╰", "│" }), + shadow = "shadow", + single = to_border_map({ "┌", "─", "┐", "│", "┘", "─", "└", "│" }), + solid = to_border_map({ " ", " ", " ", " ", " ", " ", " ", " " }), +} + +---@param style nui_popup_border_option_style +---@param prev_char_map? _nui_popup_border_internal_char +---@return _nui_popup_border_style_map +local function prepare_char_map(style, prev_char_map) + if type(style) == "string" then + if not styles[style] then + error("invalid border style name") + end + + ---@cast style _nui_popup_border_style_builtin + return vim.deepcopy(styles[style]) + end + + if is_type("list", style) then + ---@cast style _nui_popup_border_style_list + return to_border_map(style) + end + + ---@cast style _nui_popup_border_style_map + return vim.tbl_extend("force", prev_char_map or {}, style) +end + +---@param internal nui_popup_border_internal +---@return nui_popup_border_internal_size +local function calculate_size_delta(internal) + ---@type nui_popup_border_internal_size + local delta = { + width = 0, + height = 0, + } + + local char = internal.char + if type(char) == "table" then + if not is_empty_char(char.top) then + delta.height = delta.height + 1 + end + + if not is_empty_char(char.bottom) then + delta.height = delta.height + 1 + end + + if not is_empty_char(char.left) then + delta.width = delta.width + 1 + end + + if not is_empty_char(char.right) then + delta.width = delta.width + 1 + end + end + + local padding = internal.padding + if padding then + if padding.top then + delta.height = delta.height + padding.top + end + + if padding.bottom then + delta.height = delta.height + padding.bottom + end + + if padding.left then + delta.width = delta.width + padding.left + end + + if padding.right then + delta.width = delta.width + padding.right + end + end + + return delta +end + +---@param border NuiPopupBorder +---@return nui_popup_border_internal_size +local function calculate_size(border) + ---@type nui_popup_border_internal_size + local size = vim.deepcopy(border.popup._.size) + + size.width = size.width + border._.size_delta.width + size.height = size.height + border._.size_delta.height + + return size +end + +---@param border NuiPopupBorder +---@return nui_popup_border_internal_position +local function calculate_position(border) + local position = vim.deepcopy(border.popup._.position) + position.col = position.col - math.floor(border._.size_delta.width / 2 + 0.5) + position.row = position.row - math.floor(border._.size_delta.height / 2 + 0.5) + return position +end + +local function adjust_popup_win_config(border) + local internal = border._ + + local popup_position = { + row = 0, + col = 0, + } + + local char = internal.char + + if type(char) == "table" then + if not is_empty_char(char.top) then + popup_position.row = popup_position.row + 1 + end + + if not is_empty_char(char.left) then + popup_position.col = popup_position.col + 1 + end + end + + local padding = internal.padding + + if padding then + if padding.top then + popup_position.row = popup_position.row + padding.top + end + + if padding.left then + popup_position.col = popup_position.col + padding.left + end + end + + local popup = border.popup + + -- luacov: disable + if not has_nvim_0_5_1 then + popup.win_config.row = internal.position.row + popup_position.row + popup.win_config.col = internal.position.col + popup_position.col + return + end + -- luacov: enable + + -- relative to the border window + popup.win_config.anchor = nil + popup.win_config.relative = "win" + popup.win_config.win = border.winid + popup.win_config.bufpos = nil + popup.win_config.row = popup_position.row + popup.win_config.col = popup_position.col +end + +--luacheck: push no max line length + +---@alias nui_t_text_align 'left'|'center'|'right' + +---@alias nui_popup_border_internal_type 'simple'|'complex' +---@alias nui_popup_border_internal_position table<'row'|'col', number> +---@alias nui_popup_border_internal_size table<'height'|'width', number> +---@alias nui_popup_border_internal_padding _nui_popup_border_option_padding_map +---@alias nui_popup_border_internal_text { top?: NuiLine|NuiText, top_align?: nui_t_text_align, bottom?: NuiLine|NuiText, bottom_align?: nui_t_text_align } +---@alias _nui_popup_border_internal_char table<_nui_popup_border_style_map_position, NuiText> + +---@alias _nui_popup_border_option_padding_list table<1|2|3|4, integer> +---@alias _nui_popup_border_option_padding_map table<'top'|'right'|'bottom'|'left', integer> +---@alias nui_popup_border_option_padding _nui_popup_border_option_padding_list|_nui_popup_border_option_padding_map + +---@alias _nui_popup_border_style_char_tuple table<1|2, string> +---@alias _nui_popup_border_style_char string|_nui_popup_border_style_char_tuple|NuiText +---@alias _nui_popup_border_style_builtin 'double'|'none'|'rounded'|'shadow'|'single'|'solid'|'default' +---@alias _nui_popup_border_style_list table<1|2|3|4|5|6|7|8, _nui_popup_border_style_char> +---@alias _nui_popup_border_style_map_position 'top_left'|'top'|'top_right'|'right'|'bottom_right'|'bottom'|'bottom_left'|'left' +---@alias _nui_popup_border_style_map table<_nui_popup_border_style_map_position, _nui_popup_border_style_char> +---@alias nui_popup_border_option_style _nui_popup_border_style_builtin|_nui_popup_border_style_list|_nui_popup_border_style_map + +---@alias _nui_popup_border_option_text_value string|NuiLine|NuiText|string[]|table<1|2, string>[] +---@alias nui_popup_border_option_text { top?: _nui_popup_border_option_text_value, top_align?: nui_t_text_align, bottom?: _nui_popup_border_option_text_value, bottom_align?: nui_t_text_align } + +--luacheck: pop + +---@class nui_popup_border_internal +---@field type nui_popup_border_internal_type +---@field style nui_popup_border_option_style +---@field char _nui_popup_border_internal_char +---@field padding? _nui_popup_border_option_padding_map +---@field position nui_popup_border_internal_position +---@field size nui_popup_border_internal_size +---@field size_delta nui_popup_border_internal_size +---@field text? nui_popup_border_internal_text +---@field lines? NuiLine[] +---@field winhighlight? string + +---@class nui_popup_border_options +---@field padding? nui_popup_border_option_padding +---@field style? nui_popup_border_option_style +---@field text? nui_popup_border_option_text + +---@class NuiPopupBorder +---@field bufnr integer +---@field private _ nui_popup_border_internal +---@field private popup NuiPopup +---@field win_config nui_popup_win_config +---@field winid number +local Border = Object("NuiPopupBorder") + +---@param popup NuiPopup +---@param options nui_popup_border_options +function Border:init(popup, options) + self.popup = popup + + self._ = { + ---@deprecated + highlight = options.highlight, + padding = normalize_option_padding(options.padding), + text = normalize_option_text(options.text), + } + + local internal = self._ + + if internal.text or internal.padding then + internal.type = "complex" + else + internal.type = "simple" + end + + self:set_style(options.style or _.get_default_winborder()) + + internal.winhighlight = calculate_winhighlight(internal, self.popup._.win_options.winhighlight) + + if internal.type == "simple" then + return self + end + + self:_buf_create() + + self.win_config = { + style = "minimal", + border = "none", + focusable = false, + zindex = self.popup.win_config.zindex, + anchor = self.popup.win_config.anchor, + } + + if type(internal.char) == "string" then + self.win_config.border = internal.char + end +end + +function Border:_open_window() + if self.winid or not self.bufnr then + return + end + + self.win_config.noautocmd = true + self.winid = vim.api.nvim_open_win(self.bufnr, false, self.win_config) + self.win_config.noautocmd = nil + assert(self.winid, "failed to create border window") + + if self._.winhighlight then + _.set_win_option(self.winid, "winhighlight", self._.winhighlight) + end + + if self.popup._.win_options.winblend then + _.set_win_option(self.winid, "winblend", self.popup._.win_options.winblend) + end + + adjust_popup_win_config(self) + + vim.api.nvim_command("redraw") +end + +function Border:_close_window() + if not self.winid then + return + end + + if vim.api.nvim_win_is_valid(self.winid) then + vim.api.nvim_win_close(self.winid, true) + end + + self.winid = nil +end + +function Border:_buf_create() + if not self.bufnr or not vim.api.nvim_buf_is_valid(self.bufnr) then + self.bufnr = vim.api.nvim_create_buf(false, true) + vim.bo[self.bufnr].modifiable = true + assert(self.bufnr, "failed to create border buffer") + end +end + +function Border:mount() + local popup = self.popup + + if not popup._.loading or popup._.mounted then + return + end + + local internal = self._ + + if internal.type == "simple" then + return + end + + self:_buf_create() + + if internal.lines then + _.render_lines(internal.lines, self.bufnr, popup.ns_id, 1, #internal.lines) + end + + self:_open_window() +end + +function Border:unmount() + local popup = self.popup + + if not popup._.loading or not popup._.mounted then + return + end + + local internal = self._ + + if internal.type == "simple" then + return + end + + if self.bufnr then + if vim.api.nvim_buf_is_valid(self.bufnr) then + _.clear_namespace(self.bufnr, self.popup.ns_id) + vim.api.nvim_buf_delete(self.bufnr, { force = true }) + end + self.bufnr = nil + end + + self:_close_window() +end + +function Border:_relayout() + local internal = self._ + + if internal.type ~= "complex" then + return + end + + if self.popup.win_config.anchor and self.popup.win_config.anchor ~= self.win_config.anchor then + self.win_config.anchor = self.popup.win_config.anchor + self.popup.win_config.anchor = nil + end + + local position = self.popup._.position + self.win_config.relative = position.relative + self.win_config.win = position.relative == "win" and position.win or nil + self.win_config.bufpos = position.bufpos + + internal.size = calculate_size(self) + self.win_config.width = internal.size.width + self.win_config.height = internal.size.height + + internal.position = calculate_position(self) + self.win_config.row = internal.position.row + self.win_config.col = internal.position.col + + internal.lines = calculate_buf_lines(internal) + + if self.winid then + vim.api.nvim_win_set_config(self.winid, self.win_config) + end + + if self.bufnr then + if internal.lines then + _.render_lines(internal.lines, self.bufnr, self.popup.ns_id, 1, #internal.lines) + end + end + + adjust_popup_win_config(self) + + vim.api.nvim_command("redraw") +end + +---@param edge "'top'" | "'bottom'" +---@param text? nil|string|NuiLine|NuiText +---@param align? nil | "'left'" | "'center'" | "'right'" +function Border:set_text(edge, text, align) + local internal = self._ + + if not internal.text then + return + end + + internal.text[edge] = normalize_border_text(text) + internal.text[edge .. "_align"] = align or internal.text[edge .. "_align"] + + if not internal.lines then + return + end + + local line = calculate_buf_edge_line( + internal, + edge, + internal.text[edge], + internal.text[edge .. "_align"] --[[@as nui_t_text_align]] + ) + + local linenr = edge == "top" and 1 or #internal.lines + + internal.lines[linenr] = line + line:render(self.bufnr, self.popup.ns_id, linenr) +end + +---@param highlight string highlight group +function Border:set_highlight(highlight) + local internal = self._ + + local winhighlight_data = _.parse_winhighlight(self.popup._.win_options.winhighlight) + winhighlight_data["FloatBorder"] = highlight + self.popup._.win_options.winhighlight = _.serialize_winhighlight(winhighlight_data) + if self.popup.winid then + _.set_win_option(self.popup.winid, "winhighlight", self.popup._.win_options.winhighlight) + end + + internal.winhighlight = calculate_winhighlight(internal, self.popup._.win_options.winhighlight) + if self.winid then + _.set_win_option(self.winid, "winhighlight", internal.winhighlight) + end +end + +---@param style nui_popup_border_option_style +function Border:set_style(style) + local internal = self._ + + if style == "default" then + style = _.get_default_winborder() + if style == "none" and internal.type == "complex" then + style = "single" + end + end + + internal.style = style + + local char = prepare_char_map(internal.style, internal.char) + + local is_borderless = type(char) == "string" + if is_borderless then + if not internal.char then -- initial + if internal.text then + error("text not supported for style:" .. char) + end + elseif internal.type == "complex" then -- subsequent + error("cannot change from previous style to " .. char) + end + end + + internal.char = normalize_char_map(char) + internal.size_delta = calculate_size_delta(internal) +end + +---@param char_map _nui_popup_border_internal_char +---@return _nui_popup_border_style_char_tuple[] +local function to_tuple_list(char_map) + ---@type _nui_popup_border_style_char_tuple[] + local border = {} + + for index, name in ipairs(index_name) do + if not char_map[name] then + error(string.format("missing named border: %s", name)) + end + + local char = char_map[name] + border[index] = { char:content(), char.extmark.hl_group } + end + + return border +end + +---@return nil|_nui_popup_border_style_builtin|_nui_popup_border_style_char_tuple[] +function Border:get() + local internal = self._ + + if internal.type ~= "simple" then + if has_nvim_0_11_0 then + return "none" + end + return nil + end + + if type(internal.char) == "string" then + return internal.char + end + + return to_tuple_list(internal.char) +end + +---@alias NuiPopupBorder.constructor fun(popup: NuiPopup, options: nui_popup_border_options): NuiPopupBorder +---@type NuiPopupBorder|NuiPopupBorder.constructor +local NuiPopupBorder = Border + +return NuiPopupBorder diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/popup/init.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/popup/init.lua new file mode 100644 index 0000000..c2077e3 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/popup/init.lua @@ -0,0 +1,426 @@ +local Border = require("nui.popup.border") +local Object = require("nui.object") +local buf_storage = require("nui.utils.buf_storage") +local autocmd = require("nui.utils.autocmd") +local keymap = require("nui.utils.keymap") + +local utils = require("nui.utils") +local _ = utils._ +local defaults = utils.defaults +local is_type = utils.is_type + +local layout_utils = require("nui.layout.utils") +local u = { + clear_namespace = _.clear_namespace, + get_next_id = _.get_next_id, + size = layout_utils.size, + position = layout_utils.position, + update_layout_config = layout_utils.update_layout_config, +} + +-- luacov: disable +-- @deprecated +---@param opacity number +---@deprecated +local function calculate_winblend(opacity) + assert(0 <= opacity, "opacity must be equal or greater than 0") + assert(opacity <= 1, "opacity must be equal or lesser than 0") + return 100 - (opacity * 100) +end +-- luacov: enable + +local function merge_default_options(options) + options.relative = defaults(options.relative, "win") + + options.enter = defaults(options.enter, false) + options.zindex = defaults(options.zindex, 50) + + options.buf_options = defaults(options.buf_options, {}) + options.win_options = defaults(options.win_options, {}) + + options.border = defaults(options.border, _.get_default_winborder()) + + return options +end + +local function normalize_options(options) + options = _.normalize_layout_options(options) + + if is_type("string", options.border) then + options.border = { + style = options.border, + } + end + + return options +end + +--luacheck: push no max line length + +---@alias nui_popup_internal_position { relative: "'cursor'"|"'editor'"|"'win'", win: number, bufpos?: number[], row: number, col: number } +---@alias nui_popup_internal_size { height: number, width: number } +---@alias nui_popup_win_config { focusable: boolean, style: "'minimal'", zindex: number, relative: "'cursor'"|"'editor'"|"'win'", win?: number, bufpos?: number[], row: number, col: number, width: number, height: number, border?: string|table, anchor?: nui_layout_option_anchor } + +--luacheck: pop + +---@class nui_popup_internal +---@field augroup table<'hide'|'unmount', string> +---@field buf_options table +---@field layout table +---@field layout_ready boolean +---@field loading boolean +---@field mounted boolean +---@field position nui_popup_internal_position +---@field size nui_popup_internal_size +---@field unmanaged_bufnr? boolean +---@field win_config nui_popup_win_config +---@field win_enter boolean +---@field win_options table + +---@class nui_popup_options +---@field border? _nui_popup_border_style_builtin|nui_popup_border_options +---@field ns_id? string|integer +---@field anchor? nui_layout_option_anchor +---@field relative? nui_layout_option_relative_type|nui_layout_option_relative +---@field position? number|string|nui_layout_option_position +---@field size? number|string|nui_layout_option_size +---@field enter? boolean +---@field focusable? boolean +---@field zindex? integer +---@field buf_options? table +---@field win_options? table +---@field bufnr? integer + +---@class NuiPopup +---@field border NuiPopupBorder +---@field bufnr integer +---@field ns_id integer +---@field private _ nui_popup_internal +---@field win_config nui_popup_win_config +---@field winid number +local Popup = Object("NuiPopup") + +---@param options nui_popup_options +function Popup:init(options) + local id = u.get_next_id() + + options = merge_default_options(options) + options = normalize_options(options) + + self._ = { + id = id, + buf_options = options.buf_options, + layout = {}, + layout_ready = false, + loading = false, + mounted = false, + win_enter = options.enter, + win_options = options.win_options, + win_config = { + focusable = options.focusable, + style = "minimal", + anchor = options.anchor, + zindex = options.zindex, + }, + augroup = { + hide = string.format("%s_hide", id), + unmount = string.format("%s_unmount", id), + }, + } + + self.win_config = self._.win_config + + self.ns_id = _.normalize_namespace_id(options.ns_id) + + if options.bufnr then + self.bufnr = options.bufnr + self._.unmanaged_bufnr = true + else + self:_buf_create() + end + + -- luacov: disable + -- @deprecated + if not self._.win_options.winblend and is_type("number", options.opacity) then + self._.win_options.winblend = calculate_winblend(options.opacity) + end + + -- @deprecated + if not self._.win_options.winhighlight and not is_type("nil", options.highlight) then + self._.win_options.winhighlight = options.highlight + end + -- luacov: enable + + self.border = Border(self, options.border) + self.win_config.border = self.border:get() + + if options.position and options.size then + self:update_layout(options) + end +end + +function Popup:_open_window() + if self.winid or not self.bufnr then + return + end + + self.win_config.noautocmd = true + self.winid = vim.api.nvim_open_win(self.bufnr, self._.win_enter, self.win_config) + self.win_config.noautocmd = nil + + vim.api.nvim_win_call(self.winid, function() + autocmd.exec("BufWinEnter", { + buffer = self.bufnr, + modeline = false, + }) + end) + + assert(self.winid, "failed to create popup window") + + _.set_win_options(self.winid, self._.win_options) +end + +function Popup:_close_window() + if not self.winid then + return + end + + if vim.api.nvim_win_is_valid(self.winid) then + vim.api.nvim_win_close(self.winid, true) + end + + self.winid = nil +end + +function Popup:_buf_create() + if not self.bufnr then + self.bufnr = vim.api.nvim_create_buf(false, true) + assert(self.bufnr, "failed to create buffer") + end +end + +function Popup:mount() + if not self._.layout_ready then + return error("layout is not ready") + end + + if self._.loading or self._.mounted then + return + end + + self._.loading = true + + autocmd.create_group(self._.augroup.hide, { clear = true }) + autocmd.create_group(self._.augroup.unmount, { clear = true }) + autocmd.create("QuitPre", { + group = self._.augroup.unmount, + buffer = self.bufnr, + callback = vim.schedule_wrap(function() + self:unmount() + end), + }, self.bufnr) + autocmd.create("BufWinEnter", { + group = self._.augroup.unmount, + buffer = self.bufnr, + callback = function() + -- When two popup using the same buffer and both of them + -- are hidden, calling `:show` for one of them fires + -- `BufWinEnter` for both of them. And in that scenario + -- one of them will not have `self.winid`. + if self.winid then + -- @todo skip registering `WinClosed` multiple times + -- for the same popup + autocmd.create("WinClosed", { + group = self._.augroup.hide, + nested = true, + pattern = tostring(self.winid), + callback = function() + self:hide() + end, + }, self.bufnr) + end + end, + }, self.bufnr) + + self.border:mount() + + self:_buf_create() + + _.set_buf_options(self.bufnr, self._.buf_options) + + self:_open_window() + + self._.loading = false + self._.mounted = true +end + +function Popup:hide() + if self._.loading or not self._.mounted then + return + end + + self._.loading = true + + pcall(autocmd.delete_group, self._.augroup.hide) + + self.border:_close_window() + + self:_close_window() + + self._.loading = false +end + +function Popup:show() + if self._.loading then + return + end + + if not self._.mounted then + return self:mount() + end + + self._.loading = true + + autocmd.create_group(self._.augroup.hide, { clear = true }) + + self.border:_open_window() + + self:_open_window() + + self._.loading = false +end + +function Popup:_buf_destroy() + if self.bufnr then + if vim.api.nvim_buf_is_valid(self.bufnr) then + u.clear_namespace(self.bufnr, self.ns_id) + if not self._.unmanaged_bufnr then + vim.api.nvim_buf_delete(self.bufnr, { force = true }) + end + end + + buf_storage.cleanup(self.bufnr) + + if not self._.unmanaged_bufnr then + self.bufnr = nil + end + end +end + +function Popup:unmount() + if self._.loading or not self._.mounted then + return + end + + self._.loading = true + + pcall(autocmd.delete_group, self._.augroup.hide) + pcall(autocmd.delete_group, self._.augroup.unmount) + + self.border:unmount() + + self:_buf_destroy() + + self:_close_window() + + self._.loading = false + self._.mounted = false +end + +-- set keymap for this popup window +---@param mode string check `:h :map-modes` +---@param key string|string[] key for the mapping +---@param handler string | fun(): nil handler for the mapping +---@param opts? table<"'expr'"|"'noremap'"|"'nowait'"|"'remap'"|"'script'"|"'silent'"|"'unique'", boolean> +---@return nil +function Popup:map(mode, key, handler, opts, ___force___) + if not self.bufnr then + error("popup buffer not found.") + end + + return keymap.set(self.bufnr, mode, key, handler, opts, ___force___) +end + +---@param mode string check `:h :map-modes` +---@param key string|string[] key for the mapping +---@return nil +function Popup:unmap(mode, key, ___force___) + if not self.bufnr then + error("popup buffer not found.") + end + + return keymap._del(self.bufnr, mode, key, ___force___) +end + +---@param event string | string[] +---@param handler string | function +---@param options? table<"'once'" | "'nested'", boolean> +function Popup:on(event, handler, options) + if not self.bufnr then + error("popup buffer not found.") + end + + autocmd.buf.define(self.bufnr, event, handler, options) +end + +---@param event? string | string[] +function Popup:off(event) + if not self.bufnr then + error("popup buffer not found.") + end + + autocmd.buf.remove(self.bufnr, nil, event) +end + +-- luacov: disable +-- @deprecated +-- Use `popup:update_layout`. +---@deprecated +function Popup:set_layout(config) + return self:update_layout(config) +end +-- luacov: enable + +---@param config? nui_layout_options +function Popup:update_layout(config) + config = config or {} + + u.update_layout_config(self._, config) + + self.border:_relayout() + + self._.layout_ready = true + + if self.winid then + -- upstream issue: https://github.com/neovim/neovim/issues/20370 + local win_config_style = self.win_config.style + ---@diagnostic disable-next-line: assign-type-mismatch + self.win_config.style = "" + vim.api.nvim_win_set_config(self.winid, self.win_config) + self.win_config.style = win_config_style + end +end + +-- luacov: disable +-- @deprecated +-- Use `popup:update_layout`. +---@deprecated +function Popup:set_size(size) + self:update_layout({ size = size }) +end +-- luacov: enable + +-- luacov: disable +-- @deprecated +-- Use `popup:update_layout`. +---@deprecated +function Popup:set_position(position, relative) + self:update_layout({ position = position, relative = relative }) +end +-- luacov: enable + +---@alias NuiPopup.constructor fun(options: nui_popup_options): NuiPopup +---@type NuiPopup|NuiPopup.constructor +local NuiPopup = Popup + +return NuiPopup diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/split/README.md b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/split/README.md new file mode 100644 index 0000000..e41739e --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/split/README.md @@ -0,0 +1,91 @@ +# Split + +Split is can be used to split your current window or editor. + +```lua +local Split = require("nui.split") + +local split = Split({ + relative = "editor", + position = "bottom", + size = "20%", +}) +``` + +You can manipulate the associated buffer and window using the +`split.bufnr` and `split.winid` properties. + +## Options + +### `ns_id` + +**Type:** `number` or `string` + +Namespace id (`number`) or name (`string`). + +### `relative` + +**Type:** `string` or `table` + +This option affects how `size` is calculated. + +**Examples** + +Split current editor screen: + +```lua +relative = "editor" +``` + +Split current window (_default_): + +```lua +relative = "win" +``` + +Split window with specific id: + +```lua +relative = { + type = "win", + winid = 42, +} +``` + +### `position` + +`position` can be one of: `"top"`, `"right"`, `"bottom"` or `"left"`. + +### `size` + +`size` can be `number` or `percentage string`. + +For `percentage string`, size is calculated according to the option `relative`. + +### `enter` + +**Type:** `boolean` + +If `false`, the split is not entered immediately after mount. + +**Examples** + +```lua +enter = false +``` + +### `buf_options` + +Table containing buffer options to set for this split. + +### `win_options` + +Table containing window options to set for this split. + +## Methods + +[Methods from `nui.popup`](/lua/nui/popup#methods) are also available for `nui.split`. + +## Wiki Page + +You can find additional documentation/examples/guides/tips-n-tricks in [nui.split wiki page](https://github.com/MunifTanjim/nui.nvim/wiki/nui.split). diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/split/init.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/split/init.lua new file mode 100644 index 0000000..0d0fa4b --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/split/init.lua @@ -0,0 +1,378 @@ +local Object = require("nui.object") +local buf_storage = require("nui.utils.buf_storage") +local autocmd = require("nui.utils.autocmd") +local keymap = require("nui.utils.keymap") +local utils = require("nui.utils") +local split_utils = require("nui.split.utils") + +local u = { + clear_namespace = utils._.clear_namespace, + get_next_id = utils._.get_next_id, + normalize_namespace_id = utils._.normalize_namespace_id, + split = split_utils, +} + +local split_direction_command_map = { + editor = { + top = "topleft", + right = "vertical botright", + bottom = "botright", + left = "vertical topleft", + }, + win = { + top = "aboveleft", + right = "vertical rightbelow", + bottom = "belowright", + left = "vertical leftabove", + }, +} + +---@param winid integer +---@param win_config _nui_split_internal_win_config +local function move_split_window(winid, win_config) + if win_config.relative == "editor" then + vim.api.nvim_win_call(winid, function() + vim.cmd("wincmd " .. ({ top = "K", right = "L", bottom = "J", left = "H" })[win_config.position]) + end) + elseif win_config.relative == "win" then + local move_options = { + vertical = win_config.position == "left" or win_config.position == "right", + rightbelow = win_config.position == "bottom" or win_config.position == "right", + } + + vim.cmd( + string.format( + "noautocmd call win_splitmove(%s, %s, #{ vertical: %s, rightbelow: %s })", + winid, + win_config.win, + move_options.vertical and 1 or 0, + move_options.rightbelow and 1 or 0 + ) + ) + end +end + +---@param winid integer +---@param win_config _nui_split_internal_win_config +local function set_win_config(winid, win_config) + if win_config.pending_changes.position then + move_split_window(winid, win_config) + end + + if win_config.pending_changes.size then + if win_config.width then + vim.api.nvim_win_set_width(winid, win_config.width) + elseif win_config.height then + vim.api.nvim_win_set_height(winid, win_config.height) + end + end + + win_config.pending_changes = {} +end + +--luacheck: push no max line length + +---@alias nui_split_option_relative_type 'editor'|'win' +---@alias nui_split_option_relative { type: nui_split_option_relative_type, winid?: number } + +---@alias nui_split_option_position "'top'"|"'right'"|"'bottom'"|"'left'" + +---@alias nui_split_option_size { height?: number|string }|{ width?: number|string } + +---@alias _nui_split_internal_relative { type: nui_split_option_relative_type, win: number } +---@alias _nui_split_internal_win_config { height?: number, width?: number, position: nui_split_option_position, relative: nui_split_option_relative, win?: integer, pending_changes: table<'position'|'size', boolean> } + +--luacheck: pop + +---@class nui_split_internal +---@field enter? boolean +---@field loading boolean +---@field mounted boolean +---@field buf_options table +---@field win_options table +---@field position nui_split_option_position +---@field relative _nui_split_internal_relative +---@field size { height?: number }|{ width?: number } +---@field win_config _nui_split_internal_win_config +---@field pending_quit? boolean +---@field augroup table<'hide'|'unmount', string> + +---@class nui_split_options +---@field ns_id? string|integer +---@field relative? nui_split_option_relative_type|nui_split_option_relative +---@field position? nui_split_option_position +---@field size? number|string|nui_split_option_size +---@field enter? boolean +---@field buf_options? table +---@field win_options? table + +---@class NuiSplit +---@field private _ nui_split_internal +---@field bufnr integer +---@field ns_id integer +---@field winid number +local Split = Object("NuiSplit") + +---@param options nui_split_options +function Split:init(options) + local id = u.get_next_id() + + options = u.split.merge_default_options(options) + options = u.split.normalize_options(options) + + self._ = { + id = id, + enter = options.enter, + buf_options = options.buf_options, + loading = false, + mounted = false, + layout = {}, + position = options.position, + size = {}, + win_options = options.win_options, + win_config = { + pending_changes = {}, + }, + augroup = { + hide = string.format("%s_hide", id), + unmount = string.format("%s_unmount", id), + }, + } + + self.ns_id = u.normalize_namespace_id(options.ns_id) + + self:_buf_create() + + self:update_layout(options) +end + +--luacheck: push no max line length + +---@param config { relative?: nui_split_option_relative_type|nui_split_option_relative, position?: nui_split_option_position, size?: number|string|nui_split_option_size } +function Split:update_layout(config) + config = config or {} + + u.split.update_layout_config(self._, config) + + if self.winid then + set_win_config(self.winid, self._.win_config) + end +end + +--luacheck: pop + +function Split:_open_window() + if self.winid or not self.bufnr then + return + end + + self.winid = vim.api.nvim_win_call(self._.relative.type == "editor" and 0 or self._.relative.win, function() + vim.api.nvim_command( + string.format( + "silent noswapfile %s %ssplit", + split_direction_command_map[self._.relative.type][self._.position], + self._.size.width or self._.size.height or "" + ) + ) + + return vim.api.nvim_get_current_win() + end) + + vim.api.nvim_win_set_buf(self.winid, self.bufnr) + + if self._.enter then + vim.api.nvim_set_current_win(self.winid) + end + + self._.win_config.pending_changes = { size = true } + set_win_config(self.winid, self._.win_config) + + utils._.set_win_options(self.winid, self._.win_options) +end + +function Split:_close_window() + if not self.winid then + return + end + + if vim.api.nvim_win_is_valid(self.winid) and not self._.pending_quit then + vim.api.nvim_win_close(self.winid, true) + end + + self.winid = nil +end + +function Split:_buf_create() + if not self.bufnr then + self.bufnr = vim.api.nvim_create_buf(false, true) + assert(self.bufnr, "failed to create buffer") + end +end + +function Split:mount() + if self._.loading or self._.mounted then + return + end + + self._.loading = true + + autocmd.create_group(self._.augroup.hide, { clear = true }) + autocmd.create_group(self._.augroup.unmount, { clear = true }) + autocmd.create("QuitPre", { + group = self._.augroup.unmount, + buffer = self.bufnr, + callback = function() + self._.pending_quit = true + vim.schedule(function() + self:unmount() + self._.pending_quit = nil + end) + end, + }, self.bufnr) + autocmd.create("BufWinEnter", { + group = self._.augroup.unmount, + buffer = self.bufnr, + callback = function() + -- When two splits using the same buffer and both of them + -- are hidden, calling `:show` for one of them fires + -- `BufWinEnter` for both of them. And in that scenario + -- one of them will not have `self.winid`. + if self.winid then + autocmd.create("WinClosed", { + group = self._.augroup.hide, + nested = true, + pattern = tostring(self.winid), + callback = function() + self:hide() + end, + }, self.bufnr) + end + end, + }, self.bufnr) + + self:_buf_create() + + utils._.set_buf_options(self.bufnr, self._.buf_options) + + self:_open_window() + + self._.loading = false + self._.mounted = true +end + +function Split:hide() + if self._.loading or not self._.mounted then + return + end + + self._.loading = true + + pcall(autocmd.delete_group, self._.augroup.hide) + + self:_close_window() + + self._.loading = false +end + +function Split:show() + if self._.loading then + return + end + + if not self._.mounted then + return self:mount() + end + + self._.loading = true + + autocmd.create_group(self._.augroup.hide, { clear = true }) + + self:_open_window() + + self._.loading = false +end + +function Split:_buf_destroy() + if self.bufnr then + if vim.api.nvim_buf_is_valid(self.bufnr) then + u.clear_namespace(self.bufnr, self.ns_id) + + if not self._.pending_quit then + vim.api.nvim_buf_delete(self.bufnr, { force = true }) + end + end + + buf_storage.cleanup(self.bufnr) + + self.bufnr = nil + end +end + +function Split:unmount() + if self._.loading or not self._.mounted then + return + end + + self._.loading = true + + pcall(autocmd.delete_group, self._.augroup.hide) + pcall(autocmd.delete_group, self._.augroup.unmount) + + self:_buf_destroy() + + self:_close_window() + + self._.loading = false + self._.mounted = false +end + +-- set keymap for this split +---@param mode string check `:h :map-modes` +---@param key string|string[] key for the mapping +---@param handler string | fun(): nil handler for the mapping +---@param opts? table<"'expr'"|"'noremap'"|"'nowait'"|"'remap'"|"'script'"|"'silent'"|"'unique'", boolean> +---@return nil +function Split:map(mode, key, handler, opts, ___force___) + if not self.bufnr then + error("split buffer not found.") + end + + return keymap.set(self.bufnr, mode, key, handler, opts, ___force___) +end + +---@param mode string check `:h :map-modes` +---@param key string|string[] key for the mapping +---@return nil +function Split:unmap(mode, key) + if not self.bufnr then + error("split buffer not found.") + end + + return keymap._del(self.bufnr, mode, key) +end + +---@param event string | string[] +---@param handler string | function +---@param options? table<"'once'" | "'nested'", boolean> +function Split:on(event, handler, options) + if not self.bufnr then + error("split buffer not found.") + end + + autocmd.buf.define(self.bufnr, event, handler, options) +end + +---@param event? string | string[] +function Split:off(event) + if not self.bufnr then + error("split buffer not found.") + end + + autocmd.buf.remove(self.bufnr, nil, event) +end + +---@alias NuiSplit.constructor fun(options: nui_split_options): NuiSplit +---@type NuiSplit|NuiSplit.constructor +local NuiSplit = Split + +return NuiSplit diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/split/utils.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/split/utils.lua new file mode 100644 index 0000000..12b4e7b --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/split/utils.lua @@ -0,0 +1,177 @@ +local utils = require("nui.utils") +local layout_utils = require("nui.layout.utils") + +local u = { + defaults = utils.defaults, + get_editor_size = utils.get_editor_size, + get_window_size = utils.get_window_size, + is_type = utils.is_type, + normalize_dimension = utils._.normalize_dimension, + size = layout_utils.size, +} + +local mod = {} + +---@param size number|string|nui_split_option_size +---@param position nui_split_option_position +---@return number|string size +local function to_split_size(size, position) + if not u.is_type("table", size) then + ---@cast size number|string + return size + end + + if position == "left" or position == "right" then + return size.width + end + + return size.height +end + +---@param options table +---@return table options +function mod.merge_default_options(options) + options.relative = u.defaults(options.relative, "win") + options.position = u.defaults(options.position, vim.go.splitbelow and "bottom" or "top") + + options.enter = u.defaults(options.enter, true) + + options.buf_options = u.defaults(options.buf_options, {}) + options.win_options = vim.tbl_extend("force", { + winfixwidth = true, + winfixheight = true, + }, u.defaults(options.win_options, {})) + + return options +end + +---@param options nui_split_options +function mod.normalize_layout_options(options) + if utils.is_type("string", options.relative) then + options.relative = { + ---@diagnostic disable-next-line: assign-type-mismatch + type = options.relative, + } + end + + return options +end + +---@param options nui_split_options +function mod.normalize_options(options) + options = mod.normalize_layout_options(options) + + return options +end + +local function parse_relative(relative, fallback_winid) + local winid = u.defaults(relative.winid, fallback_winid) + + return { + type = relative.type, + win = winid, + } +end + +---@param relative _nui_split_internal_relative +---@return { size: { height: integer, width: integer }, type: 'editor'|'window' } +local function get_container_info(relative) + if relative.type == "editor" then + local size = u.get_editor_size() + + -- best effort adjustments + size.height = size.height - vim.api.nvim_get_option("cmdheight") + if vim.api.nvim_get_option("laststatus") >= 2 then + size.height = size.height - 1 + end + if vim.api.nvim_get_option("showtabline") == 2 then + size.height = size.height - 1 + end + + return { + size = size, + type = "editor", + } + end + + return { + size = u.get_window_size(relative.win), + type = "window", + } +end + +---@param position nui_split_option_position +---@param size number|string +---@param container_size { width: number, height: number } +---@return { width?: number, height?: number } +function mod.calculate_window_size(position, size, container_size) + if not size then + return {} + end + + if position == "left" or position == "right" then + return { + width = u.normalize_dimension(size, container_size.width), + } + end + + return { + height = u.normalize_dimension(size, container_size.height), + } +end + +function mod.update_layout_config(component_internal, config) + local internal = component_internal + + local options = mod.normalize_layout_options({ + relative = config.relative, + position = config.position, + size = config.size, + }) + + if internal.relative and internal.relative.win and not vim.api.nvim_win_is_valid(internal.relative.win) then + internal.relative.win = vim.api.nvim_get_current_win() + + internal.win_config.win = internal.relative.win + + internal.win_config.pending_changes.relative = true + end + + if options.relative then + local fallback_winid = internal.relative and internal.relative.win or vim.api.nvim_get_current_win() + internal.relative = parse_relative(options.relative, fallback_winid) + + local prev_relative = internal.win_config.relative + local prev_win = internal.win_config.win + + internal.win_config.relative = internal.relative.type + internal.win_config.win = internal.relative.type == "win" and internal.relative.win or nil + + internal.win_config.pending_changes.relative = internal.win_config.relative ~= prev_relative + or internal.win_config.win ~= prev_win + end + + if options.position or internal.win_config.pending_changes.relative then + local prev_position = internal.win_config.position + + internal.position = options.position or internal.position + + internal.win_config.position = internal.position + + internal.win_config.pending_changes.position = internal.win_config.position ~= prev_position + end + + if options.size or internal.win_config.pending_changes.position or internal.win_config.pending_changes.relative then + internal.layout.size = to_split_size(options.size or internal.layout.size, internal.position) + + internal.container_info = get_container_info(internal.relative) + internal.size = mod.calculate_window_size(internal.position, internal.layout.size, internal.container_info.size) + + internal.win_config.width = internal.size.width + internal.win_config.height = internal.size.height + + internal.win_config.pending_changes.size = true + end +end + +return mod diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/table/README.md b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/table/README.md new file mode 100644 index 0000000..0c0ef6b --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/table/README.md @@ -0,0 +1,115 @@ +# NuiTable + +NuiTable can render table-like structured content on the buffer. + +**Examples** + +```lua +local NuiTable = require("nui.table") + +local tbl = NuiTable({ + bufnr = bufnr, + columns = { + { + align = "center", + header = "Name", + columns = { + { accessor_key = "firstName", header = "First" }, + { + id = "lastName", + accessor_fn = function(row) + return row.lastName + end, + header = "Last", + }, + }, + }, + { + align = "right", + accessor_key = "age", + cell = function(cell) + return Text(tostring(cell.get_value()), "DiagnosticInfo") + end, + header = "Age", + }, + }, + data = { + { firstName = "John", lastName = "Doe", age = 42 }, + { firstName = "Jane", lastName = "Doe", age = 27 }, + }, +}) + +tbl:render() +``` + +## Options + +### `bufnr` + +**Type:** `number` + +Id of the buffer where the table will be rendered. + +--- + +### `ns_id` + +**Type:** `number` or `string` + +Namespace id (`number`) or name (`string`). + +--- + +### `columns` + +**Type:** `NuiTable.ColumnDef[]` + +List of `NuiTable.ColumnDef` objects. + +--- + +### `data` + +**Type:** `any[]` + +List of data items. + +## Methods + +### `tbl:get_cell` + +_Signature:_ `tbl:get_cell(position?: {integer, integer}) -> NuiTable.Cell | nil` + +**Parameters** + +| Name | Type | Description | +| ---------- | ---------------------- | ------------------------------------- | +| `position` | `{ integer, integer }` | `(row, col)` tuple relative to cursor | + +Returns the `NuiTable.Cell` if found. + +### `tbl:refresh_cell` + +_Signature:_ `tbl:refresh_cell(cell: NuiTable.Cell) -> nil` + +Refreshes the `cell` on buffer. + +**Parameters** + +| Name | Type | Description | +| ------ | --------------- | ----------- | +| `cell` | `NuiTable.Cell` | cell | + +### `tbl:render` + +_Signature:_ `tbl:render(linenr_start?: integer) -> nil` + +Renders the table on buffer. + +| Name | Type | Description | +| -------------- | ----------------- | ----------------------------- | +| `linenr_start` | `integer` / `nil` | start line number (1-indexed) | + +## Wiki Page + +You can find additional documentation/examples/guides/tips-n-tricks in [nui.table wiki page](https://github.com/MunifTanjim/nui.nvim/wiki/nui.table). diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/table/init.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/table/init.lua new file mode 100644 index 0000000..ff31a3f --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/table/init.lua @@ -0,0 +1,675 @@ +local Object = require("nui.object") +local Text = require("nui.text") +local Line = require("nui.line") +local _ = require("nui.utils")._ + +-- luacheck: push no max comment line length + +---@alias nui_table_border_char_name 'down_right'|'hor'|'down_hor'|'down_left'|'ver'|'ver_left'|'ver_hor'|'ver_left'|'up_right'|'up_hor'|'up_left' + +---@alias _nui_table_header_kind +---| -1 -- footer +---| 1 -- header + +---@class nui_t_list: { [integer]: T, len: integer } + +-- luacheck: pop + +---@type table +local default_border = { + hor = "─", + ver = "│", + down_right = "┌", + down_hor = "┬", + down_left = "┐", + ver_right = "├", + ver_hor = "┼", + ver_left = "┤", + up_right = "└", + up_hor = "┴", + up_left = "┘", +} + +---@param internal nui_table_internal +---@param columns NuiTable.ColumnDef[] +---@param parent? NuiTable.ColumnDef +---@param depth? integer +local function prepare_columns(internal, columns, parent, depth) + for _, col in ipairs(columns) do + if col.header then + internal.has_header = true + end + + if col.footer then + internal.has_footer = true + end + + if not col.id then + if col.accessor_key then + col.id = col.accessor_key + elseif type(col.header) == "string" then + col.id = col.header --[[@as string]] + elseif type(col.header) == "table" then + col.id = (col.header --[[@as NuiText|NuiLine]]):content() + end + end + + if not col.id then + error("missing column id") + end + + if col.accessor_key and not col.accessor_fn then + col.accessor_fn = function(row) + return row[col.accessor_key] + end + end + + col.depth = depth or 0 + col.parent = parent + + if parent and not col.header then + col.header = col.id + internal.has_header = true + end + + if col.columns then + prepare_columns(internal, col.columns, col, col.depth + 1) + else + table.insert(internal.columns, col) + end + + if col.depth == 0 then + table.insert(internal.headers, col) + else + internal.headers.depth = math.max(internal.headers.depth, col.depth + 1) + end + + if not col.align then + col.align = "left" + end + + if not col.width then + col.width = 0 + end + end +end + +---@class NuiTable.ColumnDef +---@field accessor_fn? fun(original_row: table, index: integer): string|NuiText|NuiLine +---@field accessor_key? string +---@field align? nui_t_text_align +---@field cell? fun(info: NuiTable.Cell): string|NuiText|NuiLine +---@field columns? NuiTable.ColumnDef[] +---@field footer? string|NuiText|NuiLine|fun(info: { column: NuiTable.Column }): string|NuiText|NuiLine +---@field header? string|NuiText|NuiLine|fun(info: { column: NuiTable.Column }): string|NuiText|NuiLine +---@field id? string +---@field max_width? integer +---@field min_width? integer +---@field width? integer + +---@class NuiTable.Column +---@field accessor_fn? fun(original_row: table, index: integer): string|NuiText|NuiLine +---@field accessor_key? string +---@field align nui_t_text_align +---@field columns? NuiTable.ColumnDef[] +---@field depth integer +---@field id string +---@field parent? NuiTable.Column +---@field width integer + +---@class NuiTable.Row +---@field id string +---@field index integer +---@field original table + +---@class NuiTable.Cell +---@field column NuiTable.Column +---@field content NuiText|NuiLine +---@field get_value fun(): string|NuiText|NuiLine +---@field row NuiTable.Row +---@field range table<1|2|3|4, integer> -- [start_row, start_col, end_row, end_col] + +---@class nui_table_internal +---@field border table +---@field buf_options table +---@field headers NuiTable.Column[]|{ depth: integer } +---@field columns NuiTable.ColumnDef[] +---@field data table[] +---@field has_header boolean +---@field has_footer boolean +---@field linenr table<1|2, integer> +---@field data_linenrs integer[] +---@field data_grid nui_t_list + +---@class nui_table_options +---@field bufnr integer +---@field ns_id integer|string +---@field columns NuiTable.ColumnDef[] +---@field data table[] + +---@class NuiTable +---@field private _ nui_table_internal +---@field bufnr integer +---@field ns_id integer +local Table = Object("NuiTable") + +---@param options nui_table_options +function Table:init(options) + if options.bufnr then + if not vim.api.nvim_buf_is_valid(options.bufnr) then + error("invalid bufnr " .. options.bufnr) + end + + self.bufnr = options.bufnr + end + + if not self.bufnr then + error("missing bufnr") + end + + self.ns_id = _.normalize_namespace_id(options.ns_id) + + local border = vim.tbl_deep_extend("keep", options.border or {}, default_border) + + self._ = { + buf_options = vim.tbl_extend("force", { + bufhidden = "hide", + buflisted = false, + buftype = "nofile", + modifiable = false, + readonly = true, + swapfile = false, + undolevels = 0, + }, options.buf_options or {}), + border = border, + + headers = { depth = 1 }, + columns = {}, + data = options.data or {}, + + has_header = false, + has_footer = false, + + linenr = {}, + data_linenrs = {}, + } + + prepare_columns(self._, options.columns or {}) + + _.set_buf_options(self.bufnr, self._.buf_options) +end + +---@param current_width integer +---@param min_width? integer +---@param max_width? integer +---@param content_width integer +local function get_col_width(current_width, min_width, max_width, content_width) + local min = math.max(content_width, min_width or 0) + return math.max(current_width, math.min(max_width or min, min)) +end + +---@generic C: table +---@param idx integer +---@param grid nui_t_list> +---@param kind _nui_table_header_kind +---@return nui_t_list header_row +local function get_header_row_at(idx, grid, kind) + local row = grid[idx] + if not row then + row = { len = 0 } + grid[idx] = row + grid.len = math.max(grid.len, kind * idx) + end + return row +end + +---@generic C: table +---@param kind _nui_table_header_kind +---@param columns (NuiTable.ColumnDef|{ depth: integer })[] +---@param grid nui_t_list> +---@param max_depth integer +local function prepare_header_grid(kind, columns, grid, max_depth) + local columns_len = #columns + for column_idx = 1, columns_len do + local column = columns[column_idx] + + local row_idx = kind + kind * column.depth + local row = get_header_row_at(row_idx, grid, kind) + + local content = kind == 1 and column.header or kind == -1 and column.footer or Text("") + if type(content) == "function" then + --[[@cast column NuiTable.Column]] + content = content({ column = column }) + --[[@cast content -function]] + end + if type(content) ~= "table" then + content = Text(content --[[@as string]]) + --[[@cast content -string]] + end + + column.width = get_col_width(column.width, column.min_width, column.max_width, content:width()) + + local cell = { + column = column, + content = content, + col_span = 1, + row_span = 1, + ridx = 1, + } + + row.len = row.len + 1 + row[row.len] = cell + + if column.columns then + cell.col_span = #column.columns + prepare_header_grid(kind, column.columns, grid, max_depth) + else + cell.row_span = max_depth - column.depth + for i = 1, cell.row_span - 1 do + local span_row = get_header_row_at(row_idx + i * kind, grid, kind) + span_row.len = span_row.len + 1 + span_row[span_row.len] = vim.tbl_extend("keep", { ridx = i + 1 }, cell) + end + end + end +end + +---@param cell NuiTable.Cell +---@return NuiText|NuiLine +local function prepare_cell_content(cell) + local column = cell.column --[[@as NuiTable.ColumnDef|NuiTable.Column]] + local content = column.cell and column.cell(cell) or cell.get_value() + if type(content) ~= "table" then + content = Text(tostring(content)) + end + return content +end + +---@return nui_t_list data_grid +---@return nui_t_list> header_grid +function Table:_prepare_grid() + ---@type nui_t_list + local data_grid = {} + + ---@type nui_t_list> + local header_grid = { len = 0 } + if self._.has_header then + prepare_header_grid(1, self._.headers, header_grid, self._.headers.depth) + end + + local rows = self._.data + local rows_len = #rows + + local columns = self._.columns + local columns_len = #columns + + for row_idx = 1, rows_len do + local data = rows[row_idx] + + data_grid[row_idx] = {} + + ---@type NuiTable.Row + local row = { + id = tostring(row_idx), + index = row_idx, + original = data, + } + + for column_idx = 1, columns_len do + local column = columns[column_idx] + + ---@type NuiTable.Cell + local cell = { + row = row, + column = column, + get_value = function() + return column.accessor_fn(row.original, row.index) + end, + } + + cell.content = prepare_cell_content(cell) + + column.width = get_col_width(column.width, column.min_width, column.max_width, cell.content:width()) + + data_grid[row_idx][column_idx] = cell + end + end + + if self._.has_footer then + prepare_header_grid(-1, self._.headers, header_grid, self._.headers.depth) + end + + for idx = -header_grid.len, header_grid.len do + for _, th in ipairs(header_grid[idx] or {}) do + local column = th.column + if column.columns then + column.width = 0 + for i = 1, th.col_span do + column.width = column.width + column.columns[i].width + end + column.width = column.width + th.col_span - 1 + end + end + end + + data_grid.len = rows_len + + return data_grid, header_grid +end + +---@param line NuiLine +---@param content NuiLine|NuiText +---@param width integer +---@param align nui_t_text_align +local function append_content(line, content, width, align) + if content._texts then + --[[@cast content NuiLine]] + _.truncate_nui_line(content, width) + else + --[[@cast content NuiText]] + _.truncate_nui_text(content, width) + end + local left_gap_width, right_gap_width = _.calculate_gap_width(align, width, content:width()) + if left_gap_width > 0 then + line:append(Text(string.rep(" ", left_gap_width))) + end + line:append(content) + if right_gap_width > 0 then + line:append(Text(string.rep(" ", right_gap_width))) + end + return line +end + +---@param kind _nui_table_header_kind +---@param lines nui_t_list +---@param grid nui_t_list> +function Table:_prepare_header_lines(kind, lines, grid) + local line_idx = lines.len + + local start_idx, end_idx = 1, grid.len + if kind == -1 then + start_idx, end_idx = -grid.len, -1 + end + + local border = self._.border + + for row_idx = start_idx, end_idx do + local row = grid[row_idx] + if not row then + break + end + + local inner_border_line = Line() + local data_line = Line() + local outer_border_line = Line() + + outer_border_line:append(kind == 1 and border.down_right or border.up_right) + + data_line:append(border.ver) + + local cells_len = #row + for cell_idx = 1, cells_len do + local prev_cell = row[cell_idx - 1] + local cell = row[cell_idx] + local next_cell = row[cell_idx + 1] + + if cell.row_span == cell.ridx then + if cell_idx == 1 or (prev_cell and prev_cell.ridx ~= prev_cell.row_span) then + inner_border_line:append(border.ver_right) + else + inner_border_line:append(border.ver_hor) + end + elseif next_cell then + inner_border_line:append(border.ver) + else + inner_border_line:append(border.ver_left) + end + + local column = cell.column + + if column.columns then + for sc_idx = 1, cell.col_span do + local sub_column = column.columns[sc_idx] + inner_border_line:append(string.rep(border.hor, sub_column.width)) + if sc_idx ~= cell.col_span then + inner_border_line:append(kind == 1 and border.down_hor or border.up_hor) + end + end + else + if cell.ridx == cell.row_span then + inner_border_line:append(string.rep(border.hor, column.width)) + else + inner_border_line:append(string.rep(" ", column.width)) + end + end + + if cell.ridx == cell.row_span then + append_content(data_line, cell.content, column.width, column.align) + else + append_content(data_line, Text(""), column.width, column.align) + end + data_line:append(border.ver) + + outer_border_line:append(string.rep(border.hor, column.width)) + outer_border_line:append(kind == 1 and border.down_hor or border.up_hor) + end + + local last_cell = row[cells_len] + if last_cell.ridx == last_cell.row_span then + inner_border_line:append(border.ver_left) + else + inner_border_line:append(border.ver) + end + + outer_border_line._texts[#outer_border_line._texts]:set(kind == 1 and border.down_left or border.up_left) + + if kind == -1 then + line_idx = line_idx + 1 + lines[line_idx] = inner_border_line + elseif row_idx == 1 then + line_idx = line_idx + 1 + lines[line_idx] = outer_border_line + end + line_idx = line_idx + 1 + lines[line_idx] = data_line + if kind == 1 then + line_idx = line_idx + 1 + lines[line_idx] = inner_border_line + elseif row_idx == -1 then + line_idx = line_idx + 1 + lines[line_idx] = outer_border_line + end + end + + lines.len = line_idx +end + +---@param linenr_start? integer start line number (1-indexed) +function Table:render(linenr_start) + if #self._.columns == 0 then + return + end + + linenr_start = math.max(1, linenr_start or self._.linenr[1] or 1) + local prev_linenr = { self._.linenr[1], self._.linenr[2] } + + local data_grid, header_grid = self:_prepare_grid() + + self._.data_grid = data_grid + + local line_idx = 0 + ---@type nui_t_list + local lines = { len = line_idx } + + self:_prepare_header_lines(1, lines, header_grid) + line_idx = lines.len + + local border = self._.border + + local rows_len = data_grid.len + + if line_idx == 0 and rows_len > 0 then + local columns = self._.columns + local columns_len = #columns + + local top_border_line = Line() + + top_border_line:append(border.down_right) + for column_idx = 1, columns_len do + local column = columns[column_idx] + top_border_line:append(string.rep(border.hor, column.width)) + if column_idx ~= columns_len then + top_border_line:append(border.down_hor) + end + end + top_border_line:append(border.down_left) + + line_idx = line_idx + 1 + lines[line_idx] = top_border_line + end + + local data_linenrs = self._.data_linenrs + + for row_idx = 1, rows_len do + local char_idx = 0 + + local is_last_line = row_idx == rows_len + local bottom_border_mid = is_last_line and border.up_hor or border.ver_hor + + local row = data_grid[row_idx] + + local data_line = Line() + local bottom_border_line = Line() + + local data_linenr = line_idx + linenr_start + data_line:append(border.ver) + char_idx = char_idx + 1 + + bottom_border_line:append(is_last_line and border.up_right or border.ver_right) + + local cells_len = #row + for cell_idx = 1, cells_len do + local cell = row[cell_idx] + + local column = cell.column + + append_content(data_line, cell.content, column.width, column.align) + data_line:append(border.ver) + cell.range = { data_linenr, char_idx, data_linenr, char_idx + column.width } + char_idx = cell.range[4] + 1 + + bottom_border_line:append(string.rep(border.hor, column.width)) + bottom_border_line:append(bottom_border_mid) + end + bottom_border_line._texts[#bottom_border_line._texts]:set(is_last_line and border.up_left or border.ver_left) + + line_idx = line_idx + 1 + lines[line_idx] = data_line + + data_linenrs[row_idx] = data_linenr + + if not is_last_line or not header_grid[-1] then + line_idx = line_idx + 1 + lines[line_idx] = bottom_border_line + end + end + + lines.len = line_idx + self:_prepare_header_lines(-1, lines, header_grid) + line_idx = lines.len + lines.len = nil + + _.set_buf_options(self.bufnr, { modifiable = true, readonly = false }) + + _.clear_namespace(self.bufnr, self.ns_id) + + -- if linenr_start was shifted downwards, + -- clear the previously rendered lines above. + _.clear_lines( + self.bufnr, + math.min(linenr_start, prev_linenr[1] or linenr_start), + prev_linenr[1] and linenr_start - 1 or 0 + ) + + -- for initial render, start inserting in a single line. + -- for subsequent renders, replace the lines from previous render. + _.render_lines(lines, self.bufnr, self.ns_id, linenr_start, prev_linenr[1] and prev_linenr[2] or linenr_start) + + _.set_buf_options(self.bufnr, { modifiable = false, readonly = true }) + + self._.linenr[1], self._.linenr[2] = linenr_start, line_idx + linenr_start - 1 +end + +---@param position? {[1]: integer, [2]: integer} +function Table:get_cell(position) + local pos = vim.fn.getcharpos(".") --[[@as integer[] ]] + local line, char = pos[2], pos[3] + + local row_idx = 0 + for idx, linenr in ipairs(self._.data_linenrs) do + if linenr == line then + row_idx = idx + break + elseif linenr > line then + break + end + end + row_idx = row_idx + (position and position[1] or 0) + + local row = self._.data_grid[row_idx] + if not row then + return + end + + local cell_idx = 0 + for idx, cell in ipairs(row) do + local range = cell.range + if range[2] < char and char <= range[4] then + cell_idx = idx + end + end + cell_idx = cell_idx + (position and position[2] or 0) + + return row[cell_idx] +end + +function Table:refresh_cell(cell) + local column = cell.column + + local range = cell.range + local byte_range = _.char_to_byte_range(self.bufnr, range[1], range[2], range[4]) + + local content = prepare_cell_content(cell) + if cell.content ~= content then + cell.content = content + + local extmarks = vim.api.nvim_buf_get_extmarks( + self.bufnr, + self.ns_id, + { range[1] - 1, byte_range[1] }, + { range[3] - 1, byte_range[2] - 1 }, + {} + ) + for _, extmark in ipairs(extmarks) do + vim.api.nvim_buf_del_extmark(self.bufnr, self.ns_id, extmark[1]) + end + end + + _.set_buf_options(self.bufnr, { modifiable = true, readonly = false }) + _.render_lines( + { append_content(Line(), content, column.width, column.align) }, + self.bufnr, + self.ns_id, + range[1], + range[3], + byte_range[1], + byte_range[2] + ) + _.set_buf_options(self.bufnr, { modifiable = false, readonly = true }) +end + +---@alias NuiTable.constructor fun(options: nui_table_options): NuiTable +---@type NuiTable|NuiTable.constructor +local NuiTable = Table + +return NuiTable diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/text/README.md b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/text/README.md new file mode 100644 index 0000000..5dc7802 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/text/README.md @@ -0,0 +1,141 @@ +# NuiText + +NuiText is an abstraction layer on top of the following native functions: + +- `vim.api.nvim_buf_set_text` (check `:h nvim_buf_set_text()`) +- `vim.api.nvim_buf_set_extmark` (check `:h nvim_buf_set_extmark()`) + +It helps you set text and add highlight for it on the buffer. + +_Signature:_ `NuiText(content, extmark?)` + +**Examples** + +```lua +local NuiText = require("nui.text") + +local text = NuiText("Something Went Wrong!", "Error") + +local bufnr, ns_id, linenr_start, byte_start = 0, -1, 1, 0 + +text:render(bufnr, ns_id, linenr_start, byte_start) +``` + +## Parameters + +### `content` + +**Type:** `string` or `table` + +Text content or `NuiText` object. + +If `NuiText` object is passed, a copy of it is created. + +### `extmark` + +**Type:** `string` or `table` + +Highlight group name or extmark options. + +If a `string` is passed, it is used as the highlight group name. + +If a `table` is passed it is used as extmark data. It can have the +following keys: + +| Key | Description | +| ------------ | -------------------- | +| `"hl_group"` | highlight group name | + +For more, check `:help nvim_buf_set_extmark()`. + +## Methods + +### `text:set` + +_Signature:_ `text:set(content, extmark?)` + +Sets the text content and highlight information. + +**Parameters** + +| Name | Type | Description | +| --------- | ------------------- | --------------------------------------- | +| `content` | `string` | text content | +| `extmark` | `string` or `table` | highlight group name or extmark options | + +This `extmark` parameter is exactly the same as `NuiText`'s `extmark` parameter. + +### `text:content` + +_Signature:_ `text:content()` + +Returns the text content. + +### `text:length` + +_Signature:_ `text:length()` + +Returns the byte length of the text. + +### `text:width` + +_Signature:_ `text:width()` + +Returns the character length of the text. + +### `text:highlight` + +_Signature:_ `text:highlight(bufnr, ns_id, linenr, byte_start)` + +Applies highlight for the text. + +**Parameters** + +| Name | Type | Description | +| ------------ | -------- | -------------------------------------------------- | +| `bufnr` | `number` | buffer number | +| `ns_id` | `number` | namespace id (use `-1` for fallback namespace) | +| `linenr` | `number` | line number (1-indexed) | +| `byte_start` | `number` | start position of the text on the line (0-indexed) | + +### `text:render` + +_Signature:_ `text:render(bufnr, ns_id, linenr_start, byte_start, linenr_end?, byte_end?)` + +Sets the text on buffer and applies highlight. + +**Parameters** + +| Name | Type | Description | +| -------------- | -------- | -------------------------------------------------- | +| `bufnr` | `number` | buffer number | +| `ns_id` | `number` | namespace id (use `-1` for fallback namespace) | +| `linenr_start` | `number` | start line number (1-indexed) | +| `byte_start` | `number` | start position of the text on the line (0-indexed) | +| `linenr_end` | `number` | end line number (1-indexed) | +| `byte_end` | `number` | end position of the text on the line (0-indexed) | + +### `text:render_char` + +_Signature:_ `text:render_char(bufnr, ns_id, linenr_start, char_start, linenr_end?, char_end?)` + +Sets the text on buffer and applies highlight. + +This does the thing as `text:render` method, but you can use character count +instead of byte count. It will convert multibyte character count to appropriate +byte count for you. + +**Parameters** + +| Name | Type | Description | +| -------------- | -------- | -------------------------------------------------- | +| `bufnr` | `number` | buffer number | +| `ns_id` | `number` | namespace id (use `-1` for fallback namespace) | +| `linenr_start` | `number` | start line number (1-indexed) | +| `char_start` | `number` | start position of the text on the line (0-indexed) | +| `linenr_end` | `number` | end line number (1-indexed) | +| `char_end` | `number` | end position of the text on the line (0-indexed) | + +## Wiki Page + +You can find additional documentation/examples/guides/tips-n-tricks in [nui.text wiki page](https://github.com/MunifTanjim/nui.nvim/wiki/nui.text). diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/text/init.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/text/init.lua new file mode 100644 index 0000000..c741326 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/text/init.lua @@ -0,0 +1,114 @@ +local Object = require("nui.object") +local _ = require("nui.utils")._ +local is_type = require("nui.utils").is_type + +---@class nui_text_extmark +---@field id? integer +---@field hl_group? string +---@field [string] any + +---@class NuiText +---@field protected extmark? nui_text_extmark +local Text = Object("NuiText") + +---@param content string|NuiText text content or NuiText object +---@param extmark? string|nui_text_extmark highlight group name or extmark options +function Text:init(content, extmark) + if type(content) == "string" then + self:set(content, extmark) + else + -- cloning + self:set(content._content, extmark or content.extmark) + end +end + +---@param content string text content +---@param extmark? string|nui_text_extmark highlight group name or extmark options +---@return NuiText +function Text:set(content, extmark) + if self._content ~= content then + self._content = content + self._length = vim.fn.strlen(content) + self._width = vim.api.nvim_strwidth(content) + end + + if extmark then + -- preserve self.extmark.id + local id = self.extmark and self.extmark.id or nil + self.extmark = is_type("string", extmark) and { hl_group = extmark } or vim.deepcopy(extmark) + self.extmark.id = id + end + + return self +end + +---@return string +function Text:content() + return self._content +end + +---@return number +function Text:length() + return self._length +end + +---@return number +function Text:width() + return self._width +end + +---@param bufnr number buffer number +---@param ns_id number namespace id +---@param linenr number line number (1-indexed) +---@param byte_start number start byte position (0-indexed) +---@return nil +function Text:highlight(bufnr, ns_id, linenr, byte_start) + if not self.extmark then + return + end + + self.extmark.end_col = byte_start + self:length() + + self.extmark.id = + vim.api.nvim_buf_set_extmark(bufnr, _.ensure_namespace_id(ns_id), linenr - 1, byte_start, self.extmark) +end + +---@param bufnr number buffer number +---@param ns_id number namespace id +---@param linenr_start number start line number (1-indexed) +---@param byte_start number start byte position (0-indexed) +---@param linenr_end? number end line number (1-indexed) +---@param byte_end? number end byte position (0-indexed) +---@return nil +function Text:render(bufnr, ns_id, linenr_start, byte_start, linenr_end, byte_end) + local row_start = linenr_start - 1 + local row_end = linenr_end and linenr_end - 1 or row_start + + local col_start = byte_start + local col_end = byte_end or byte_start + self:length() + + local content = self:content() + + vim.api.nvim_buf_set_text(bufnr, row_start, col_start, row_end, col_end, { content }) + + self:highlight(bufnr, ns_id, linenr_start, byte_start) +end + +---@param bufnr number buffer number +---@param ns_id number namespace id +---@param linenr_start number start line number (1-indexed) +---@param char_start number start character position (0-indexed) +---@param linenr_end? number end line number (1-indexed) +---@param char_end? number end character position (0-indexed) +---@return nil +function Text:render_char(bufnr, ns_id, linenr_start, char_start, linenr_end, char_end) + char_end = char_end or char_start + self:width() + local byte_range = _.char_to_byte_range(bufnr, linenr_start, char_start, char_end) + self:render(bufnr, ns_id, linenr_start, byte_range[1], linenr_end, byte_range[2]) +end + +---@alias NuiText.constructor fun(content: string|NuiText, extmark?: string|nui_text_extmark): NuiText +---@type NuiText|NuiText.constructor +local NuiText = Text + +return NuiText diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/tree/README.md b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/tree/README.md new file mode 100644 index 0000000..48e73fa --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/tree/README.md @@ -0,0 +1,308 @@ +# NuiTree + +NuiTree can render tree-like structured content on the buffer. + +**Examples** + +```lua +local NuiTree = require("nui.tree") + +local tree = NuiTree({ + bufnr = bufnr, + nodes = { + NuiTree.Node({ text = "a" }), + NuiTree.Node({ text = "b" }, { + NuiTree.Node({ text = "b-1" }), + NuiTree.Node({ text = { "b-2", "b-3" } }), + }), + }, +}) + +tree:render() +``` + +## Options + +### `bufnr` + +**Type:** `number` + +Id of the buffer where the tree will be rendered. + +--- + +### `ns_id` + +**Type:** `number` or `string` + +Namespace id (`number`) or name (`string`). + +--- + +### `nodes` + +**Type:** `table` + +List of [`NuiTree.Node`](#nuitreenode) objects. + +--- + +### `get_node_id` + +**Type:** `function` + +_Signature:_ `get_node_id(node) -> string` + +If provided, this function is used for generating node's id. + +The return value should be a unique `string`. + +**Example** + +```lua +get_node_id = function(node) + if node.id then + return "-" .. node.id + end + + if node.text then + return string.format("%s-%s-%s", node:get_parent_id() or "", node:get_depth(), node.text) + end + + return "-" .. math.random() +end, +``` + +--- + +### `prepare_node` + +**Type:** `function` + +_Signature:_ `prepare_node(node, parent_node?) -> nil | string | string[] | NuiLine | NuiLine[]` + +If provided, this function is used for preparing each node line. + +The return value should be a `NuiLine` object or `string` or a list containing either of them. + +If return value is `nil`, that node will not be rendered. + +**Example** + +```lua +prepare_node = function(node) + local line = NuiLine() + + line:append(string.rep(" ", node:get_depth() - 1)) + + if node:has_children() then + line:append(node:is_expanded() and " " or " ") + else + line:append(" ") + end + + line:append(node.text) + + return line +end, +``` + +--- + +### `buf_options` + +**Type:** `table` + +Contains all buffer related options (check `:h options | /local to buffer`). + +**Examples** + +```lua +buf_options = { + bufhidden = "hide", + buflisted = false, + buftype = "nofile", + swapfile = false, +}, +``` + +## Methods + +### `tree:get_node` + +_Signature:_ `tree:get_node(node_id_or_linenr?) -> NuiTreeNode | nil, number | nil, number | nil` + +**Parameters** + +| Name | Type | Description | +| ------------------- | ----------------------------- | ------------------------ | +| `node_id_or_linenr` | `number` or `string` or `nil` | node's id or line number | + +If `node_id_or_linenr` is `string`, the node with that _id_ is returned. + +If `node_id_or_linenr` is `number`, the node on that _linenr_ is returned. + +If `node_id` is `nil`, the current node under cursor is returned. + +Returns the `node` if found, and the start and end `linenr` if it is rendered. + +### `tree:get_nodes` + +_Signature:_ `tree:get_node(parent_id?) -> NuiTreeNode[]` + +**Parameters** + +| Name | Type | Description | +| ----------- | ----------------- | ---------------- | +| `parent_id` | `string` or `nil` | parent node's id | + +If `parent_id` is present, child nodes under that parent are returned, +Otherwise root nodes are returned. + +### `tree:add_node` + +_Signature:_ `tree:add_node(node, parent_id?)` + +Adds a node to the tree. + +| Name | Type | Description | +| ----------- | ----------------- | ---------------- | +| `node` | `NuiTree.Node` | node | +| `parent_id` | `string` or `nil` | parent node's id | + +If `parent_id` is present, node is added under that parent, +Otherwise node is added to the tree root. + +### `tree:remove_node` + +_Signature:_ `tree:remove_node(node)` + +Removes a node from the tree. + +Returns the removed node. + +| Name | Type | Description | +| --------- | -------- | ----------- | +| `node_id` | `string` | node's id | + +### `tree:set_nodes` + +_Signature:_ `tree:set_nodes(nodes, parent_id?)` + +Adds a node to the tree. + +| Name | Type | Description | +| ----------- | ----------------- | ---------------- | +| `nodes` | `NuiTree.Node[]` | list of nodes | +| `parent_id` | `string` or `nil` | parent node's id | + +If `parent_id` is present, nodes are set as parent node's children, +otherwise nodes are set at tree root. + +### `tree:render` + +_Signature:_ `tree:render(linenr_start?)` + +Renders the tree on buffer. + +| Name | Type | Description | +| -------------- | ---------------- | ----------------------------- | +| `linenr_start` | `number` / `nil` | start line number (1-indexed) | + +## NuiTree.Node + +`NuiTree.Node` is used to create a node object for `NuiTree`. + +_Signature:_ `NuiTree.Node(data, children)` + +**Examples** + +```lua +local NuiTree = require("nui.tree") + +local node = NuiTree.Node({ text = "b" }, { + NuiTree.Node({ text = "b-1" }), + NuiTree.Node({ text = "b-2" }), +}) +``` + +### Parameters + +#### `data` + +**Type:** `table` + +Data for the node. Can contain anything. The default `get_node_id` +and `prepare_node` functions uses the `id` and `text` keys. + +**Example** + +```lua +{ + id = "/usr/local/bin/lua", + text = "lua" +} +``` + +If you don't want to provide those two values, you should consider +providing your own `get_node_id` and `prepare_node` functions. + +#### `children` + +**Type:** `table` + +List of `NuiTree.Node` objects. + +### Methods + +#### `node:get_id` + +_Signature:_ `node:get_id()` + +Returns node's id. + +#### `node:get_depth` + +_Signature:_ `node:get_depth()` + +Returns node's depth. + +#### `node:get_parent_id` + +_Signature:_ `node:get_parent_id()` + +Returns parent node's id. + +#### `node:has_children` + +_Signature:_ `node:has_children()` + +Checks if node has children. + +#### `node:get_child_ids` + +_Signature:_ `node:get_child_ids() -> string[]` + +Returns ids of child nodes. + +#### `node:is_expanded` + +_Signature:_ `node:is_expanded()` + +Checks if node is expanded. + +#### `node:expand` + +_Signature:_ `node:expand()` + +Expands node. + +#### `node:collapse` + +_Signature:_ `node:collapse()` + +Collapses node. + +## Wiki Page + +You can find additional documentation/examples/guides/tips-n-tricks in [nui.tree wiki page](https://github.com/MunifTanjim/nui.nvim/wiki/nui.tree). diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/tree/init.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/tree/init.lua new file mode 100644 index 0000000..60bd4f6 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/tree/init.lua @@ -0,0 +1,482 @@ +local Object = require("nui.object") +local _ = require("nui.utils")._ +local defaults = require("nui.utils").defaults +local is_type = require("nui.utils").is_type +local tree_util = require("nui.tree.util") + +-- returns id of the first window that contains the buffer +---@param bufnr number +---@return number winid +local function get_winid(bufnr) + return vim.fn.win_findbuf(bufnr)[1] +end + +---@param nodes NuiTree.Node[] +---@param parent_node? NuiTree.Node +---@param get_node_id nui_tree_get_node_id +---@return { by_id: table, root_ids: string[] } +local function initialize_nodes(nodes, parent_node, get_node_id) + local start_depth = parent_node and parent_node:get_depth() + 1 or 1 + + ---@type table + local by_id = {} + ---@type string[] + local root_ids = {} + + ---@param node NuiTree.Node + ---@param depth number + local function initialize(node, depth) + node._depth = depth + node._id = get_node_id(node) + node._initialized = true + + local node_id = node:get_id() + + if by_id[node_id] then + error("duplicate node id " .. node_id) + end + + by_id[node_id] = node + + if depth == start_depth then + table.insert(root_ids, node_id) + end + + if not node.__children or #node.__children == 0 then + return + end + + if not node._child_ids then + node._child_ids = {} + end + + for _, child_node in ipairs(node.__children) do + child_node._parent_id = node_id + initialize(child_node, depth + 1) + table.insert(node._child_ids, child_node:get_id()) + end + + node.__children = nil + end + + for _, node in ipairs(nodes) do + node._parent_id = parent_node and parent_node:get_id() or nil + initialize(node, start_depth) + end + + return { + by_id = by_id, + root_ids = root_ids, + } +end + +---@class NuiTree.Node +---@field _id string +---@field _depth integer +---@field _parent_id? string +---@field _child_ids? string[] +---@field __children? NuiTree.Node[] +---@field [string] any +local TreeNode = { + super = nil, +} + +---@alias NuiTreeNode NuiTree.Node + +---@return string +function TreeNode:get_id() + return self._id +end + +---@return number +function TreeNode:get_depth() + return self._depth +end + +---@return string|nil +function TreeNode:get_parent_id() + return self._parent_id +end + +---@return boolean +function TreeNode:has_children() + local items = self._child_ids or self.__children + return items and #items > 0 or false +end + +---@return string[] +function TreeNode:get_child_ids() + return self._child_ids or {} +end + +---@return boolean +function TreeNode:is_expanded() + return self._is_expanded +end + +---@return boolean is_updated +function TreeNode:expand() + if (self._child_ids or self.__children) and not self:is_expanded() then + self._is_expanded = true + return true + end + return false +end + +---@return boolean is_updated +function TreeNode:collapse() + if self:is_expanded() then + self._is_expanded = false + return true + end + return false +end + +--luacheck: push no max line length + +---@alias nui_tree_get_node_id fun(node: NuiTree.Node): string +---@alias nui_tree_prepare_node fun(node: NuiTree.Node, parent_node?: NuiTree.Node): nil | string | string[] | NuiLine | NuiLine[] + +--luacheck: pop + +---@class nui_tree_internal +---@field buf_options table +---@field get_node_id nui_tree_get_node_id +---@field linenr { [1]?: integer, [2]?: integer } +---@field linenr_by_node_id table +---@field node_id_by_linenr table +---@field prepare_node nui_tree_prepare_node +---@field win_options table # deprecated + +---@class nui_tree_options +---@field bufnr integer +---@field ns_id? string|integer +---@field nodes? NuiTree.Node[] +---@field get_node_id? fun(node: NuiTree.Node): string +---@field prepare_node? fun(node: NuiTree.Node, parent_node?: NuiTree.Node): nil|string|string[]|NuiLine|NuiLine[] + +---@class NuiTree +---@field bufnr integer +---@field nodes { by_id: table, root_ids: string[] } +---@field ns_id integer +---@field private _ nui_tree_internal +---@field winid number # @deprecated +local Tree = Object("NuiTree") + +---@param options nui_tree_options +function Tree:init(options) + ---@deprecated + if options.winid then + if not vim.api.nvim_win_is_valid(options.winid) then + error("invalid winid " .. options.winid) + end + + self.winid = options.winid + self.bufnr = vim.api.nvim_win_get_buf(self.winid) + end + + if options.bufnr then + if not vim.api.nvim_buf_is_valid(options.bufnr) then + error("invalid bufnr " .. options.bufnr) + end + + self.bufnr = options.bufnr + self.winid = nil + end + + if not self.bufnr then + error("missing bufnr") + end + + self.ns_id = _.normalize_namespace_id(options.ns_id) + + self._ = { + buf_options = vim.tbl_extend("force", { + bufhidden = "hide", + buflisted = false, + buftype = "nofile", + modifiable = false, + readonly = true, + swapfile = false, + undolevels = 0, + }, defaults(options.buf_options, {})), + ---@deprecated + win_options = vim.tbl_extend("force", { + foldcolumn = "0", + foldmethod = "manual", + wrap = false, + }, defaults(options.win_options, {})), + get_node_id = defaults(options.get_node_id, tree_util.default_get_node_id), + prepare_node = defaults(options.prepare_node, tree_util.default_prepare_node), + + linenr = {}, + } + + _.set_buf_options(self.bufnr, self._.buf_options) + + ---@deprecated + if self.winid then + _.set_win_options(self.winid, self._.win_options) + end + + self:set_nodes(defaults(options.nodes, {})) +end + +---@generic D : table +---@param data D data table +---@param children? NuiTree.Node[] +---@return NuiTree.Node|D +function Tree.Node(data, children) + ---@type NuiTree.Node + local self = { + __children = children, + _initialized = false, + _is_expanded = false, + _child_ids = nil, + _parent_id = nil, + ---@diagnostic disable-next-line: assign-type-mismatch + _depth = nil, + ---@diagnostic disable-next-line: assign-type-mismatch + _id = nil, + } + + self = setmetatable(vim.tbl_extend("keep", self, data), { + __index = TreeNode, + __name = "NuiTree.Node", + }) + + return self +end + +---@param node_id_or_linenr? string | integer +---@return NuiTree.Node|nil node +---@return nil|integer linenr +---@return nil|integer linenr +function Tree:get_node(node_id_or_linenr) + if is_type("string", node_id_or_linenr) then + return self.nodes.by_id[node_id_or_linenr], unpack(self._.linenr_by_node_id[node_id_or_linenr] or {}) + end + + local winid = get_winid(self.bufnr) + local linenr = node_id_or_linenr or vim.api.nvim_win_get_cursor(winid)[1] + local node_id = self._.node_id_by_linenr[linenr] + return self.nodes.by_id[node_id], unpack(self._.linenr_by_node_id[node_id] or {}) +end + +---@param parent_id? string parent node's id +---@return NuiTree.Node[] nodes +function Tree:get_nodes(parent_id) + local node_ids = {} + + if parent_id then + local parent_node = self.nodes.by_id[parent_id] + if parent_node then + node_ids = parent_node._child_ids + end + else + node_ids = self.nodes.root_ids + end + + return vim.tbl_map(function(id) + return self.nodes.by_id[id] + end, node_ids or {}) +end + +---@param nodes NuiTree.Node[] +---@param parent_node? NuiTree.Node +function Tree:_add_nodes(nodes, parent_node) + local new_nodes = initialize_nodes(nodes, parent_node, self._.get_node_id) + + self.nodes.by_id = vim.tbl_extend("force", self.nodes.by_id, new_nodes.by_id) + + if parent_node then + if not parent_node._child_ids then + parent_node._child_ids = {} + end + + for _, id in ipairs(new_nodes.root_ids) do + table.insert(parent_node._child_ids, id) + end + else + for _, id in ipairs(new_nodes.root_ids) do + table.insert(self.nodes.root_ids, id) + end + end +end + +---@param nodes NuiTree.Node[] +---@param parent_id? string parent node's id +function Tree:set_nodes(nodes, parent_id) + self._.node_id_by_linenr = {} + self._.linenr_by_node_id = {} + + if not parent_id then + self.nodes = { by_id = {}, root_ids = {} } + self:_add_nodes(nodes) + return + end + + local parent_node = self.nodes.by_id[parent_id] + if not parent_node then + error("invalid parent_id " .. parent_id) + end + + if parent_node._child_ids then + for _, node_id in ipairs(parent_node._child_ids) do + self.nodes.by_id[node_id] = nil + end + + parent_node._child_ids = nil + end + + self:_add_nodes(nodes, parent_node) +end + +---@param node NuiTree.Node +---@param parent_id? string parent node's id +function Tree:add_node(node, parent_id) + local parent_node = self.nodes.by_id[parent_id] + if parent_id and not parent_node then + error("invalid parent_id " .. parent_id) + end + + self:_add_nodes({ node }, parent_node) +end + +local function remove_node(tree, node_id) + local node = tree.nodes.by_id[node_id] + if node:has_children() then + for _, child_id in ipairs(node._child_ids) do + -- We might want to store the nodes and return them with the node itself? + -- We should _really_ not be doing this recursively, but it will work for now + remove_node(tree, child_id) + end + end + tree.nodes.by_id[node_id] = nil + return node +end + +---@param node_id string +---@return NuiTree.Node +function Tree:remove_node(node_id) + local node = remove_node(self, node_id) + local parent_id = node._parent_id + if parent_id then + local parent_node = self.nodes.by_id[parent_id] + parent_node._child_ids = vim.tbl_filter(function(id) + return id ~= node_id + end, parent_node._child_ids) + else + self.nodes.root_ids = vim.tbl_filter(function(id) + return id ~= node_id + end, self.nodes.root_ids) + end + return node +end + +---@param linenr_start number start line number (1-indexed) +---@return (string|NuiLine)[]|{ len: integer } lines +function Tree:_prepare_content(linenr_start) + local internal = self._ + + local by_id = self.nodes.by_id + + ---@type { [1]: string|NuiLine } + local list_wrapper = {} + + local tree_linenr = 0 + local lines = { len = tree_linenr } + + local node_id_by_linenr = {} + internal.node_id_by_linenr = node_id_by_linenr + + local linenr_by_node_id = {} + internal.linenr_by_node_id = linenr_by_node_id + + local function prepare(node_id, parent_node) + local node = by_id[node_id] + if not node then + return + end + + local node_lines = internal.prepare_node(node, parent_node) + if node_lines then + if type(node_lines) ~= "table" or node_lines.content then + list_wrapper[1] = node_lines + node_lines = list_wrapper + end + ---@cast node_lines -string, -NuiLine + + local node_linenr = linenr_by_node_id[node_id] or {} + for node_line_idx = 1, #node_lines do + local node_line = node_lines[node_line_idx] + + tree_linenr = tree_linenr + 1 + local buffer_linenr = tree_linenr + linenr_start - 1 + + lines[tree_linenr] = node_line + + node_id_by_linenr[buffer_linenr] = node_id + + if node_line_idx == 1 then + node_linenr[1] = buffer_linenr + end + node_linenr[2] = buffer_linenr + end + linenr_by_node_id[node_id] = node_linenr + end + + local child_ids = node._child_ids + if child_ids and node._is_expanded then + for child_id_idx = 1, #child_ids do + prepare(child_ids[child_id_idx], node) + end + end + end + + local root_ids = self.nodes.root_ids + for node_id_idx = 1, #root_ids do + prepare(root_ids[node_id_idx]) + end + + lines.len = tree_linenr + + return lines +end + +---@param linenr_start? number start line number (1-indexed) +function Tree:render(linenr_start) + linenr_start = math.max(1, linenr_start or self._.linenr[1] or 1) + + local prev_linenr = { self._.linenr[1], self._.linenr[2] } + + local lines = self:_prepare_content(linenr_start) + local line_idx = lines.len + lines.len = nil + + _.set_buf_options(self.bufnr, { modifiable = true, readonly = false }) + + _.clear_namespace(self.bufnr, self.ns_id, prev_linenr[1], prev_linenr[2]) + + -- if linenr_start was shifted downwards, + -- clear the previously rendered lines above. + _.clear_lines( + self.bufnr, + math.min(linenr_start, prev_linenr[1] or linenr_start), + prev_linenr[1] and linenr_start - 1 or 0 + ) + + -- for initial render, start inserting in a single line. + -- for subsequent renders, replace the lines from previous render. + _.render_lines(lines, self.bufnr, self.ns_id, linenr_start, prev_linenr[1] and prev_linenr[2] or linenr_start) + + _.set_buf_options(self.bufnr, { modifiable = false, readonly = true }) + + self._.linenr[1], self._.linenr[2] = linenr_start, line_idx + linenr_start - 1 +end + +---@alias NuiTree.constructor fun(options: nui_tree_options): NuiTree +---@type NuiTree|NuiTree.constructor +local NuiTree = Tree + +return NuiTree diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/tree/util.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/tree/util.lua new file mode 100644 index 0000000..4e95613 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/tree/util.lua @@ -0,0 +1,70 @@ +local NuiLine = require("nui.line") + +local mod = {} + +---@param node NuiTree.Node +---@return string node_id +function mod.default_get_node_id(node) + if node.id then + return "-" .. node.id + end + + if node.text then + local texts = node.text + if type(node.text) ~= "table" or node.text.content then + texts = { node.text } + end + return string.format( + "%s-%s-%s", + node._parent_id or "", + node._depth, + table.concat( + vim.tbl_map(function(text) + if type(text) == "string" then + return text + end + return text:content() + end, texts), + "-" + ) + ) + end + + return "-" .. math.random() +end + +---@param node NuiTree.Node +---@return NuiLine[] +function mod.default_prepare_node(node) + if not node.text then + error("missing node.text") + end + + local texts = node.text + + if type(node.text) ~= "table" or node.text.content then + texts = { node.text } + end + + local lines = {} + + for i, text in ipairs(texts) do + local line = NuiLine() + + line:append(string.rep(" ", node._depth - 1)) + + if i == 1 and node:has_children() then + line:append(node:is_expanded() and " " or " ") + else + line:append(" ") + end + + line:append(text) + + table.insert(lines, line) + end + + return lines +end + +return mod diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/utils/autocmd.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/utils/autocmd.lua new file mode 100644 index 0000000..c0e1891 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/utils/autocmd.lua @@ -0,0 +1,478 @@ +local buf_storage = require("nui.utils.buf_storage") +local is_type = require("nui.utils").is_type +local feature = require("nui.utils")._.feature + +local autocmd = { + event = { + -- after adding a buffer to the buffer list + BufAdd = "BufAdd", + -- deleting a buffer from the buffer list + BufDelete = "BufDelete", + -- after entering a buffer + BufEnter = "BufEnter", + -- after renaming a buffer + BufFilePost = "BufFilePost", + -- before renaming a buffer + BufFilePre = "BufFilePre", + -- just after buffer becomes hidden + BufHidden = "BufHidden", + -- before leaving a buffer + BufLeave = "BufLeave", + -- after the 'modified' state of a buffer changes + BufModifiedSet = "BufModifiedSet", + -- after creating any buffer + BufNew = "BufNew", + -- when creating a buffer for a new file + BufNewFile = "BufNewFile", + -- read buffer using command + BufReadCmd = "BufReadCmd", + -- after reading a buffer + BufReadPost = "BufReadPost", + -- before reading a buffer + BufReadPre = "BufReadPre", + -- just before unloading a buffer + BufUnload = "BufUnload", + -- after showing a buffer in a window + BufWinEnter = "BufWinEnter", + -- just after buffer removed from window + BufWinLeave = "BufWinLeave", + -- just before really deleting a buffer + BufWipeout = "BufWipeout", + -- write buffer using command + BufWriteCmd = "BufWriteCmd", + -- after writing a buffer + BufWritePost = "BufWritePost", + -- before writing a buffer + BufWritePre = "BufWritePre", + -- info was received about channel + ChanInfo = "ChanInfo", + -- channel was opened + ChanOpen = "ChanOpen", + -- command undefined + CmdUndefined = "CmdUndefined", + -- command line was modified + CmdlineChanged = "CmdlineChanged", + -- after entering cmdline mode + CmdlineEnter = "CmdlineEnter", + -- before leaving cmdline mode + CmdlineLeave = "CmdlineLeave", + -- after entering the cmdline window + CmdWinEnter = "CmdwinEnter", + -- before leaving the cmdline window + CmdWinLeave = "CmdwinLeave", + -- after loading a colorscheme + ColorScheme = "ColorScheme", + -- before loading a colorscheme + ColorSchemePre = "ColorSchemePre", + -- after popup menu changed + CompleteChanged = "CompleteChanged", + -- after finishing insert complete + CompleteDone = "CompleteDone", + -- idem, before clearing info + CompleteDonePre = "CompleteDonePre", + -- cursor in same position for a while + CursorHold = "CursorHold", + -- idem, in Insert mode + CursorHoldI = "CursorHoldI", + -- cursor was moved + CursorMoved = "CursorMoved", + -- cursor was moved in Insert mode + CursorMovedI = "CursorMovedI", + -- diffs have been updated + DiffUpdated = "DiffUpdated", + -- directory changed + DirChanged = "DirChanged", + -- after changing the 'encoding' option + EncodingChanged = "EncodingChanged", + -- before exiting + ExitPre = "ExitPre", + -- append to a file using command + FileAppendCmd = "FileAppendCmd", + -- after appending to a file + FileAppendPost = "FileAppendPost", + -- before appending to a file + FileAppendPre = "FileAppendPre", + -- before first change to read-only file + FileChangedRO = "FileChangedRO", + -- after shell command that changed file + FileChangedShell = "FileChangedShell", + -- after (not) reloading changed file + FileChangedShellPost = "FileChangedShellPost", + -- read from a file using command + FileReadCmd = "FileReadCmd", + -- after reading a file + FileReadPost = "FileReadPost", + -- before reading a file + FileReadPre = "FileReadPre", + -- new file type detected (user defined) + FileType = "FileType", + -- write to a file using command + FileWriteCmd = "FileWriteCmd", + -- after writing a file + FileWritePost = "FileWritePost", + -- before writing a file + FileWritePre = "FileWritePre", + -- after reading from a filter + FilterReadPost = "FilterReadPost", + -- before reading from a filter + FilterReadPre = "FilterReadPre", + -- after writing to a filter + FilterWritePost = "FilterWritePost", + -- before writing to a filter + FilterWritePre = "FilterWritePre", + -- got the focus + FocusGained = "FocusGained", + -- lost the focus to another app + FocusLost = "FocusLost", + -- if calling a function which doesn't exist + FuncUndefined = "FuncUndefined", + -- after starting the GUI + GUIEnter = "GUIEnter", + -- after starting the GUI failed + GUIFailed = "GUIFailed", + -- when changing Insert/Replace mode + InsertChange = "InsertChange", + -- before inserting a char + InsertCharPre = "InsertCharPre", + -- when entering Insert mode + InsertEnter = "InsertEnter", + -- just after leaving Insert mode + InsertLeave = "InsertLeave", + -- just before leaving Insert mode + InsertLeavePre = "InsertLeavePre", + -- just before popup menu is displayed + MenuPopup = "MenuPopup", + -- after changing the mode + ModeChanged = "ModeChanged", + -- after setting any option + OptionSet = "OptionSet", + -- after :make, :grep etc. + QuickFixCmdPost = "QuickFixCmdPost", + -- before :make, :grep etc. + QuickFixCmdPre = "QuickFixCmdPre", + -- before :quit + QuitPre = "QuitPre", + -- upon string reception from a remote vim + RemoteReply = "RemoteReply", + -- when the search wraps around the document + SearchWrapped = "SearchWrapped", + -- after loading a session file + SessionLoadPost = "SessionLoadPost", + -- after ":!cmd" + ShellCmdPost = "ShellCmdPost", + -- after ":1,2!cmd", ":w !cmd", ":r !cmd". + ShellFilterPost = "ShellFilterPost", + -- after nvim process received a signal + Signal = "Signal", + -- sourcing a Vim script using command + SourceCmd = "SourceCmd", + -- after sourcing a Vim script + SourcePost = "SourcePost", + -- before sourcing a Vim script + SourcePre = "SourcePre", + -- spell file missing + SpellFileMissing = "SpellFileMissing", + -- after reading from stdin + StdinReadPost = "StdinReadPost", + -- before reading from stdin + StdinReadPre = "StdinReadPre", + -- found existing swap file + SwapExists = "SwapExists", + -- syntax selected + Syntax = "Syntax", + -- a tab has closed + TabClosed = "TabClosed", + -- after entering a tab page + TabEnter = "TabEnter", + -- before leaving a tab page + TabLeave = "TabLeave", + -- when creating a new tab + TabNew = "TabNew", + -- after entering a new tab + TabNewEntered = "TabNewEntered", + -- after changing 'term' + TermChanged = "TermChanged", + -- after the process exits + TermClose = "TermClose", + -- after entering Terminal mode + TermEnter = "TermEnter", + -- after leaving Terminal mode + TermLeave = "TermLeave", + -- after opening a terminal buffer + TermOpen = "TermOpen", + -- after setting "v:termresponse" + TermResponse = "TermResponse", + -- text was modified + TextChanged = "TextChanged", + -- text was modified in Insert mode(no popup) + TextChangedI = "TextChangedI", + -- text was modified in Insert mode(popup) + TextChangedP = "TextChangedP", + -- after a yank or delete was done (y, d, c) + TextYankPost = "TextYankPost", + -- after UI attaches + UIEnter = "UIEnter", + -- after UI detaches + UILeave = "UILeave", + -- user defined autocommand + User = "User", + -- whenthe user presses the same key 42 times + UserGettingBored = "UserGettingBored", + -- after starting Vim + VimEnter = "VimEnter", + -- before exiting Vim + VimLeave = "VimLeave", + -- before exiting Vim and writing ShaDa file + VimLeavePre = "VimLeavePre", + -- after Vim window was resized + VimResized = "VimResized", + -- after Nvim is resumed + VimResume = "VimResume", + -- before Nvim is suspended + VimSuspend = "VimSuspend", + -- after closing a window + WinClosed = "WinClosed", + -- after entering a window + WinEnter = "WinEnter", + -- before leaving a window + WinLeave = "WinLeave", + -- when entering a new window + WinNew = "WinNew", + -- after scrolling a window + WinScrolled = "WinScrolled", + + -- alias for `BufAdd` + BufCreate = "BufAdd", + -- alias for `BufReadPost` + BufRead = "BufReadPost", + -- alias for `BufWritePre` + BufWrite = "BufWritePre", + -- alias for `EncodingChanged` + FileEncoding = "EncodingChanged", + }, + buf = { + storage = buf_storage.create("nui.utils.autocmd", { _next_handler_id = 1 }), + }, +} + +---@param callback fun(event: table): nil +---@param bufnr integer +local function to_stored_handler(callback, bufnr) + local handler_id = autocmd.buf.storage[bufnr]._next_handler_id + autocmd.buf.storage[bufnr]._next_handler_id = handler_id + 1 + + autocmd.buf.storage[bufnr][handler_id] = callback + + local command = string.format(":lua require('nui.utils.autocmd').execute_stored_handler(%s, %s)", bufnr, handler_id) + + return command +end + +---@param bufnr integer +---@param handler_id number +function autocmd.execute_stored_handler(bufnr, handler_id) + local handler = autocmd.buf.storage[bufnr][handler_id] + if is_type("function", handler) then + handler() + end +end + +---@param name string +---@param opts { clear?: boolean } +function autocmd.create_group(name, opts) + if feature.lua_autocmd then + return vim.api.nvim_create_augroup(name, opts) + end + + vim.cmd(string.format( + [[ + augroup %s + %s + augroup end + ]], + name, + opts.clear and "autocmd!" or "" + )) +end + +---@param name string +function autocmd.delete_group(name) + if feature.lua_autocmd then + return vim.api.nvim_del_augroup_by_name(name) + end + + vim.cmd(string.format( + [[ + autocmd! %s + augroup! %s + ]], + name, + name + )) +end + +---@param event string|string[] +---@param opts table +---@param bufnr? integer # to store callback if lua autocmd is not available +function autocmd.create(event, opts, bufnr) + if feature.lua_autocmd then + return vim.api.nvim_create_autocmd(event, opts) + end + + event = type(event) == "table" and table.concat(event, ",") or event --[[@as string]] + local pattern = is_type("table", opts.pattern) and table.concat(opts.pattern, ",") or opts.pattern + if opts.buffer then + pattern = string.format("", opts.buffer) + end + + if opts.callback then + local buffer = opts.buffer or bufnr + if not buffer then + error("[nui.utils.autocmd] missing param: bufnr") + end + opts.command = to_stored_handler(opts.callback, buffer) + end + + vim.cmd( + string.format( + "autocmd %s %s %s %s %s %s", + opts.group or "", + event, + pattern, + opts.once and "++once" or "", + opts.nested and "++nested" or "", + opts.command + ) + ) +end + +---@param opts table +function autocmd.delete(opts) + if feature.lua_autocmd then + for _, item in ipairs(vim.api.nvim_get_autocmds(opts)) do + if item.id then + vim.api.nvim_del_autocmd(item.id) + end + end + + return + end + + local event = is_type("table", opts.event) and table.concat(opts.event, ",") or opts.event + local pattern = is_type("table", opts.pattern) and table.concat(opts.pattern, ",") or opts.pattern + if opts.buffer then + pattern = string.format("", opts.buffer) + end + + vim.cmd(string.format("autocmd! %s %s %s", opts.group or "", event or "*", pattern or "")) +end + +---@param event string|string[] +---@param opts table +function autocmd.exec(event, opts) + local events = type(event) == "table" and event or { event } --[=[@as string[]]=] + + if feature.lua_autocmd then + vim.api.nvim_exec_autocmds(events, { + group = opts.group, + pattern = opts.pattern, + buffer = opts.buffer, + modeline = opts.modeline, + data = opts.data, + }) + + return + end + + for _, event_name in ipairs(events) do + local command = string.format( + [[doautocmd %s %s %s %s]], + opts.modeline == false and "" or "", + opts.group or "", + event_name, + opts.pattern or "" + ) + + if opts.buffer then + vim.api.nvim_buf_call(opts.buffer, function() + vim.cmd(command) + end) + else + vim.cmd(command) + end + end +end + +-- @deprecated +---@deprecated +---@param event string | string[] +---@param pattern string | string[] +---@param cmd string +---@param options nil | table<"'once'" | "'nested'", boolean> +function autocmd.define(event, pattern, cmd, options) + local opts = options or {} + opts.pattern = pattern + opts.command = cmd + autocmd.create(event, opts) +end + +-- @deprecated +---@deprecated +---@param group_name string +---@param auto_clear boolean +---@param definitions table<"'event'" | "'pattern'" | "'cmd'" | "'options'", any> +function autocmd.define_grouped(group_name, auto_clear, definitions) + if not is_type("boolean", auto_clear) then + error("invalid param type: auto_clear, expected boolean") + end + + autocmd.create_group(group_name, { clear = auto_clear }) + + for _, definition in ipairs(definitions) do + autocmd.define(definition.event, definition.pattern, definition.cmd, definition.options) + end +end + +-- @deprecated +---@deprecated +---@param group_name nil | string +---@param event nil | string | string[] +---@param pattern nil | string | string[] +function autocmd.remove(group_name, event, pattern) + autocmd.delete({ + event = event, + group = group_name, + pattern = pattern, + }) +end + +---@param bufnr number +---@param event string | string[] +---@param handler string | function +---@param options nil | table<"'once'" | "'nested'", boolean> +function autocmd.buf.define(bufnr, event, handler, options) + local opts = options or {} + + opts.buffer = bufnr + + if is_type("function", handler) then + opts.callback = handler + else + opts.command = handler + end + + autocmd.create(event, opts, bufnr) +end + +---@param bufnr number +---@param group_name nil | string +---@param event nil | string | string[] +function autocmd.buf.remove(bufnr, group_name, event) + autocmd.delete({ + buffer = bufnr, + event = event, + group = group_name, + }) +end + +return autocmd diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/utils/buf_storage.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/utils/buf_storage.lua new file mode 100644 index 0000000..4845b64 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/utils/buf_storage.lua @@ -0,0 +1,33 @@ +local defaults = require("nui.utils").defaults + +local buf_storage = { + _registry = {}, +} + +---@param storage_name string +---@param default_value any +---@return table +function buf_storage.create(storage_name, default_value) + local storage = setmetatable({}, { + __index = function(tbl, bufnr) + rawset(tbl, bufnr, vim.deepcopy(defaults(default_value, {}))) + + -- TODO: can `buf_storage.cleanup` be automatically (and reliably) triggered on `BufWipeout`? + + return tbl[bufnr] + end, + }) + + buf_storage._registry[storage_name] = storage + + return storage +end + +---@param bufnr number +function buf_storage.cleanup(bufnr) + for _, storage in pairs(buf_storage._registry) do + rawset(storage, bufnr, nil) + end +end + +return buf_storage diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/utils/init.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/utils/init.lua new file mode 100644 index 0000000..cd721ce --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/utils/init.lua @@ -0,0 +1,387 @@ +local ok_nvim_version, nvim_version = pcall(vim.version) +if not ok_nvim_version then + nvim_version = {} +end + +-- internal utils +local _ = { + feature = { + lua_keymap = type(vim.keymap) ~= "nil", + lua_autocmd = type(vim.api.nvim_create_autocmd) ~= "nil", + v0_10 = nvim_version.minor >= 10, + v0_11 = nvim_version.minor >= 11, + }, +} + +local utils = { + _ = _, +} + +function utils.get_editor_size() + return { + width = vim.o.columns, + height = vim.o.lines, + } +end + +function utils.get_window_size(winid) + winid = winid or 0 + return { + width = vim.api.nvim_win_get_width(winid), + height = vim.api.nvim_win_get_height(winid), + } +end + +function utils.defaults(v, default_value) + return type(v) == "nil" and default_value or v +end + +-- luacheck: push no max comment line length +---@param type_name "'nil'" | "'number'" | "'string'" | "'boolean'" | "'table'" | "'function'" | "'thread'" | "'userdata'" | "'list'" | '"map"' +---@return boolean +function utils.is_type(type_name, v) + -- `vim.tbl_islist` will be removed in the future + local islist = vim.islist or vim.tbl_islist + if type_name == "list" then + return islist(v) + end + + if type_name == "map" then + return type(v) == "table" and not islist(v) + end + + return type(v) == type_name +end +-- luacheck: pop + +---@param v string | number +function utils.parse_number_input(v) + local parsed = {} + + parsed.is_percentage = type(v) == "string" and string.sub(v, -1) == "%" + + if parsed.is_percentage then + parsed.value = tonumber(string.sub(v, 1, #v - 1)) / 100 + else + parsed.value = tonumber(v) + parsed.is_percentage = parsed.value and 0 < parsed.value and parsed.value < 1 + end + + return parsed +end + +---@param prefix? string +---@return (fun(): string) get_next_id +local function get_id_generator(prefix) + prefix = prefix or "" + local id = 0 + return function() + id = id + 1 + return prefix .. id + end +end + +_.get_next_id = get_id_generator("nui_") + +---@private +---@param bufnr number +---@param linenr number line number (1-indexed) +---@param char_start number start character position (0-indexed) +---@param char_end number end character position (0-indexed) +---@return number[] byte_range +function _.char_to_byte_range(bufnr, linenr, char_start, char_end) + local line = vim.api.nvim_buf_get_lines(bufnr, linenr - 1, linenr, false)[1] + local skipped_part = vim.fn.strcharpart(line, 0, char_start) + local target_part = vim.fn.strcharpart(line, char_start, char_end - char_start) + + local byte_start = vim.fn.strlen(skipped_part) + local byte_end = math.min(byte_start + vim.fn.strlen(target_part), vim.fn.strlen(line)) + return { byte_start, byte_end } +end + +---@type integer +local fallback_namespace_id = vim.api.nvim_create_namespace("nui.nvim") + +---@private +---@param ns_id integer +---@return integer +function _.ensure_namespace_id(ns_id) + return ns_id == -1 and fallback_namespace_id or ns_id +end + +---@private +---@param ns_id? integer|string +---@return integer ns_id namespace id +function _.normalize_namespace_id(ns_id) + if utils.is_type("string", ns_id) then + ---@cast ns_id string + return vim.api.nvim_create_namespace(ns_id) + end + ---@cast ns_id integer + return ns_id or fallback_namespace_id +end + +---@private +---@param bufnr integer +---@param ns_id integer +---@param linenr_start? integer (1-indexed) +---@param linenr_end? integer (1-indexed,inclusive) +function _.clear_namespace(bufnr, ns_id, linenr_start, linenr_end) + linenr_start = linenr_start or 1 + linenr_end = linenr_end and linenr_end + 1 or 0 + vim.api.nvim_buf_clear_namespace(bufnr, ns_id, linenr_start - 1, linenr_end - 1) +end + +-- luacov: disable +local nvim_buf_set_option = vim.api.nvim_buf_set_option +---@param bufnr integer +---@param name string +---@param value any +local function set_buf_option(bufnr, name, value) + nvim_buf_set_option(bufnr, name, value) +end + +local nvim_win_set_option = vim.api.nvim_win_set_option +---@param winid integer +---@param name string +---@param value any +local function set_win_option(winid, name, value) + nvim_win_set_option(winid, name, value) +end +-- luacov: enable + +if _.feature.v0_10 then + function set_buf_option(bufnr, name, value) + vim.api.nvim_set_option_value(name, value, { buf = bufnr }) + end + + function set_win_option(winid, name, value) + vim.api.nvim_set_option_value(name, value, { win = winid, scope = "local" }) + end +end + +_.set_buf_option = set_buf_option +_.set_win_option = set_win_option + +---@private +---@param bufnr number +---@param buf_options table +function _.set_buf_options(bufnr, buf_options) + for name, value in pairs(buf_options) do + set_buf_option(bufnr, name, value) + end +end + +---@private +---@param winid number +---@param win_options table +function _.set_win_options(winid, win_options) + for name, value in pairs(win_options) do + set_win_option(winid, name, value) + end +end + +---@private +---@param dimension number | string +---@param container_dimension number +---@return nil | number +function _.normalize_dimension(dimension, container_dimension) + local number = utils.parse_number_input(dimension) + + if not number.value then + return nil + end + + if number.is_percentage then + return math.floor(container_dimension * number.value) + end + + return number.value +end + +local strchars, strcharpart, strdisplaywidth = vim.fn.strchars, vim.fn.strcharpart, vim.fn.strdisplaywidth + +---@param text string +---@param max_length number +---@return string +function _.truncate_text(text, max_length) + if strdisplaywidth(text) <= max_length then + return text + end + + local low, high = 0, strchars(text) + local mid + + while low < high do + mid = math.floor((low + high + 1) / 2) + if strdisplaywidth(strcharpart(text, 0, mid)) < max_length then + low = mid + else + high = mid - 1 + end + end + + return strcharpart(text, 0, low) .. "…" +end + +---@param text NuiText +---@param max_width number +function _.truncate_nui_text(text, max_width) + text:set(_.truncate_text(text:content(), max_width)) +end + +---@param line NuiLine +---@param max_width number +function _.truncate_nui_line(line, max_width) + local width = line:width() + local last_part_idx = #line._texts + + while width > max_width do + local extra_width = width - max_width + local last_part = line._texts[last_part_idx] + + if last_part:width() <= extra_width then + width = width - last_part:width() + line._texts[last_part_idx] = nil + last_part_idx = last_part_idx - 1 + + -- need to add truncate indicator in previous part + if last_part:width() == extra_width then + last_part = line._texts[last_part_idx] + last_part:set(_.truncate_text(last_part:content() .. " ", last_part:width())) + end + else + last_part:set(_.truncate_text(last_part:content(), last_part:width() - extra_width)) + width = width - extra_width + end + end +end + +---@param align "'left'" | "'center'" | "'right'" +---@param total_width number +---@param text_width number +---@return number left_gap_width, number right_gap_width +function _.calculate_gap_width(align, total_width, text_width) + local gap_width = total_width - text_width + if align == "left" then + return 0, gap_width + elseif align == "center" then + return math.floor(gap_width / 2), math.ceil(gap_width / 2) + elseif align == "right" then + return gap_width, 0 + end + + error("invalid value align=" .. align) +end + +---@param lines (string|NuiLine)[] +---@param bufnr number +---@param ns_id number +---@param linenr_start integer (1-indexed) +---@param linenr_end? integer (1-indexed,inclusive) +---@param byte_start? integer (0-indexed) +---@param byte_end? integer (0-indexed,exclusive) +function _.render_lines(lines, bufnr, ns_id, linenr_start, linenr_end, byte_start, byte_end) + local row_start = linenr_start - 1 + local row_end = linenr_end or row_start + 1 + + local content = vim.tbl_map(function(line) + if type(line) == "string" then + return line + end + return line:content() + end, lines) + + if byte_start then + local col_start = byte_start + local col_end = byte_end or #vim.api.nvim_buf_get_lines(bufnr, row_start, row_end, false)[1] + vim.api.nvim_buf_set_text(bufnr, row_start, col_start, row_end - 1, col_end, content) + else + vim.api.nvim_buf_set_lines(bufnr, row_start, row_end, false, content) + end + + for linenr, line in ipairs(lines) do + if type(line) ~= "string" then + line:highlight(bufnr, ns_id, linenr + row_start, byte_start) + end + end +end + +---@param bufnr integer +---@param linenr_start integer (1-indexed) +---@param linenr_end integer (1-indexed,inclusive) +function _.clear_lines(bufnr, linenr_start, linenr_end) + local count = linenr_end - linenr_start + 1 + if count < 1 then + return + end + + local lines = {} + for i = 1, count do + lines[i] = "" + end + + vim.api.nvim_buf_set_lines(bufnr, linenr_start - 1, linenr_end, false, lines) +end + +function _.normalize_layout_options(options) + if utils.is_type("string", options.relative) then + options.relative = { + type = options.relative, + } + end + + if options.position and not utils.is_type("table", options.position) then + options.position = { + row = options.position, + col = options.position, + } + end + + if options.size and not utils.is_type("table", options.size) then + options.size = { + width = options.size, + height = options.size, + } + end + + return options +end + +---@param winhighlight string +---@return table highlight_map +function _.parse_winhighlight(winhighlight) + local highlight = {} + local parts = vim.split(winhighlight, ",", { plain = true, trimempty = true }) + for _, part in ipairs(parts) do + local key, value = part:match("(.+):(.+)") + highlight[key] = value + end + return highlight +end + +---@param highlight_map table +---@return string winhighlight +function _.serialize_winhighlight(highlight_map) + local parts = vim.tbl_map(function(key) + return key .. ":" .. highlight_map[key] + end, vim.tbl_keys(highlight_map)) + table.sort(parts) + return table.concat(parts, ",") +end + +function _.get_default_winborder() + return "none" +end + +if _.feature.v0_11 then + function _.get_default_winborder() + local style = vim.api.nvim_get_option_value("winborder", {}) + if style == "" then + return "none" + end + return style + end +end + +return utils diff --git a/.config/nvim/pack/tree/start/nui.nvim/lua/nui/utils/keymap.lua b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/utils/keymap.lua new file mode 100644 index 0000000..94e7c3a --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/lua/nui/utils/keymap.lua @@ -0,0 +1,154 @@ +local buf_storage = require("nui.utils.buf_storage") +local is_type = require("nui.utils").is_type +local feature = require("nui.utils")._.feature + +local keymap = { + storage = buf_storage.create("nui.utils.keymap", { _next_handler_id = 1, keys = {}, handlers = {} }), +} + +---@param mode string +---@param key string +---@return string key_id +local function get_key_id(mode, key) + return string.format("%s---%s", mode, vim.api.nvim_replace_termcodes(key, true, true, true)) +end + +---@param bufnr number +---@param key_id string +---@return integer|nil handler_id +local function get_handler_id(bufnr, key_id) + return keymap.storage[bufnr].keys[key_id] +end + +---@param bufnr number +---@param key_id string +---@return integer handler_id +local function next_handler_id(bufnr, key_id) + local handler_id = keymap.storage[bufnr]._next_handler_id + keymap.storage[bufnr].keys[key_id] = handler_id + keymap.storage[bufnr]._next_handler_id = handler_id + 1 + return handler_id +end + +---@param bufnr number +---@param mode string +---@param key string +---@param handler string|fun(): nil +---@return { rhs: string, callback?: fun(): nil }|nil +local function get_keymap_info(bufnr, mode, key, handler, overwrite) + local key_id = get_key_id(mode, key) + + -- luacov: disable + if get_handler_id(bufnr, key_id) and not overwrite then + return nil + end + -- luacov: enable + + local handler_id = next_handler_id(bufnr, key_id) + + local rhs, callback = "", nil + + if type(handler) == "function" then + if feature.lua_keymap then + callback = handler + else + keymap.storage[bufnr].handlers[handler_id] = handler + rhs = string.format("lua require('nui.utils.keymap').execute(%s, %s)", bufnr, handler_id) + end + else + rhs = handler + end + + return { + rhs = rhs, + callback = callback, + } +end + +---@param bufnr number +---@param handler_id number +function keymap.execute(bufnr, handler_id) + local handler = keymap.storage[bufnr].handlers[handler_id] + if is_type("function", handler) then + handler(bufnr) + end +end + +---@param bufnr number +---@param mode string +---@param lhs string|string[] +---@param handler string|fun(): nil +---@param opts? table<"'expr'"|"'noremap'"|"'nowait'"|"'remap'"|"'script'"|"'silent'"|"'unique'", boolean> +---@return nil +function keymap.set(bufnr, mode, lhs, handler, opts, force) + if feature.lua_keymap and not is_type("boolean", force) then + force = true + end + + local keys = lhs + if type(lhs) ~= "table" then + keys = { lhs } + end + ---@cast keys -string + + opts = opts or {} + + if not is_type("nil", opts.remap) then + opts.noremap = not opts.remap + opts.remap = nil + end + + for _, key in ipairs(keys) do + local keymap_info = get_keymap_info(bufnr, mode, key, handler, force) + -- luacov: disable + if not keymap_info then + return false + end + -- luacov: enable + + local options = vim.deepcopy(opts) + options.callback = keymap_info.callback + + vim.api.nvim_buf_set_keymap(bufnr, mode, key, keymap_info.rhs, options) + end + + return true +end + +---@param bufnr number +---@param mode string +---@param lhs string|string[] +---@return nil +function keymap._del(bufnr, mode, lhs, force) + if feature.lua_keymap and not is_type("boolean", force) then + force = true + end + + local keys = lhs + if type(lhs) ~= "table" then + keys = { lhs } + end + ---@cast keys -string + + for _, key in ipairs(keys) do + local key_id = get_key_id(mode, key) + + local handler_id = get_handler_id(bufnr, key_id) + + -- luacov: disable + if not handler_id and not force then + return false + ---@cast handler_id -nil + end + -- luacov: enable + + keymap.storage[bufnr].keys[key_id] = nil + keymap.storage[bufnr].handlers[handler_id] = nil + + vim.api.nvim_buf_del_keymap(bufnr, mode, key) + end + + return true +end + +return keymap diff --git a/.config/nvim/pack/tree/start/nui.nvim/nui.nvim-dev-1.rockspec b/.config/nvim/pack/tree/start/nui.nvim/nui.nvim-dev-1.rockspec new file mode 100644 index 0000000..5661c29 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/nui.nvim-dev-1.rockspec @@ -0,0 +1,27 @@ +rockspec_format = "3.0" +package = "nui.nvim" +version = "dev-1" +source = { + url = "git+https://github.com/MunifTanjim/nui.nvim.git", + tag = nil, +} +description = { + summary = "UI Component Library for Neovim.", + detailed = [[ + UI Component Library for Neovim. + ]], + license = "MIT", + homepage = "https://github.com/MunifTanjim/nui.nvim", + issues_url = "https://github.com/MunifTanjim/nui.nvim/issues", + maintainer = "Munif Tanjim (https://muniftanjim.dev)", + labels = { + "neovim", + }, +} +build = { + type = "builtin", +} +test = { + type = "command", + command = "scripts/test.sh", +} diff --git a/.config/nvim/pack/tree/start/nui.nvim/scripts/format.sh b/.config/nvim/pack/tree/start/nui.nvim/scripts/format.sh new file mode 100755 index 0000000..773b1f6 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/scripts/format.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env sh + +r_v_major="0" +r_v_minor="17" +r_v_patch="1" + +v="$(stylua --version | cut -d' ' -f2)" +v_major="$(echo "${v}" | cut -d'.' -f1)" +v_minor="$(echo "${v}" | cut -d'.' -f2)" +v_patch="$(echo "${v}" | cut -d'.' -f3)" + +v_error_message="required stylua ~v${r_v_major}.${r_v_minor}.${r_v_patch}, found v${v_major}.${v_minor}.${v_patch}" + +if test ${v_major} -ne ${r_v_major} || test ${v_minor} -ne ${r_v_minor} || test ${v_patch} -lt ${r_v_patch}; then + echo ${v_error_message} >&2 + exit 1 +fi + +stylua --color always ${1} lua/nui/ tests/ diff --git a/.config/nvim/pack/tree/start/nui.nvim/scripts/lint.sh b/.config/nvim/pack/tree/start/nui.nvim/scripts/lint.sh new file mode 100755 index 0000000..706c7eb --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/scripts/lint.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +luacheck $@ . diff --git a/.config/nvim/pack/tree/start/nui.nvim/scripts/plenary-353.patch b/.config/nvim/pack/tree/start/nui.nvim/scripts/plenary-353.patch new file mode 100644 index 0000000..646b010 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/scripts/plenary-353.patch @@ -0,0 +1,35 @@ +diff --git a/lua/plenary/busted.lua b/lua/plenary/busted.lua +index 1b15fce..8363084 100644 +--- a/lua/plenary/busted.lua ++++ b/lua/plenary/busted.lua +@@ -238,7 +238,7 @@ mod.run = function(file) + -- If nothing runs (empty file without top level describe) + if not results.pass then + if is_headless then +- return vim.cmd "0cq" ++ os.exit(0) + else + return + end +@@ -259,7 +259,7 @@ mod.run = function(file) + end + else + if is_headless then +- return vim.cmd "0cq" ++ os.exit(0) + end + end + end)() +diff --git a/lua/plenary/test_harness.lua b/lua/plenary/test_harness.lua +index 394e28d..66cc6b4 100644 +--- a/lua/plenary/test_harness.lua ++++ b/lua/plenary/test_harness.lua +@@ -169,7 +169,7 @@ function harness.test_directory(directory, opts) + return vim.cmd "1cq" + end + +- return vim.cmd "0cq" ++ os.exit(0) + end + end + diff --git a/.config/nvim/pack/tree/start/nui.nvim/scripts/test.sh b/.config/nvim/pack/tree/start/nui.nvim/scripts/test.sh new file mode 100755 index 0000000..9a51385 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/scripts/test.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash + +set -euo pipefail + +declare -r plugins_dir="./.tests/site/pack/deps/start" +declare -r module="nui" + +declare test_scope="${module}" + +while [[ $# -gt 0 ]]; do + case "${1}" in + --clean) + shift + echo "[test] cleaning up environment" + rm -rf "${plugins_dir}" + echo "[test] envionment cleaned" + ;; + *) + if [[ "${test_scope}" == "${module}" ]] && [[ "${1}" == "${module}/"* ]]; then + test_scope="${1}" + fi + shift + ;; + esac +done + +function setup_environment() { + echo + echo "[test] setting up environment" + echo + + if [[ ! -d "${plugins_dir}" ]]; then + mkdir -p "${plugins_dir}" + fi + + if [[ ! -d "${plugins_dir}/plenary.nvim" ]]; then + echo "[plugins] plenary.nvim: installing..." + git clone https://github.com/nvim-lua/plenary.nvim "${plugins_dir}/plenary.nvim" + # commit 9069d14a120cadb4f6825f76821533f2babcab92 broke luacov + # issue: https://github.com/nvim-lua/plenary.nvim/issues/353 + local -r plenary_353_patch="$(pwd)/scripts/plenary-353.patch" + git -C "${plugins_dir}/plenary.nvim" apply "${plenary_353_patch}" + echo "[plugins] plenary.nvim: installed" + echo + fi + + echo "[test] environment ready" + echo +} + +function luacov_start() { + luacov_dir="$(dirname "$(luarocks which luacov 2>/dev/null | head -1)")" + if [[ "${luacov_dir}" == "." ]]; then + luacov_dir="" + fi + + if test -n "${luacov_dir}"; then + rm -f luacov.*.out + export LUA_PATH=";;${luacov_dir}/?.lua" + fi +} + +function luacov_end() { + if test -n "${luacov_dir}"; then + if test -f "luacov.stats.out"; then + luacov + + echo + tail -n +$(($(grep -n "^Summary$" luacov.report.out | cut -d":" -f1) - 1)) luacov.report.out + fi + fi +} + +setup_environment + +luacov_start + +declare test_logs="" + +if [[ -d "./tests/${test_scope}/" ]]; then + test_logs=$(nvim --headless --noplugin -u tests/init.lua -c "lua require('plenary.test_harness').test_directory('./tests/${test_scope}/', { minimal_init = 'tests/init.lua', sequential = true })" || true) +elif [[ -f "./tests/${test_scope}_spec.lua" ]]; then + test_logs=$(nvim --headless --noplugin -u tests/init.lua -c "lua require('plenary.busted').run('./tests/${test_scope}_spec.lua')" || true) +fi + +echo "${test_logs}" + +luacov_end + +if echo "${test_logs}" | grep --quiet "stack traceback"; then + { + echo "" + echo "FOUND STACK TRACEBACK IN TEST LOGS" + echo "" + } >&2 + exit 1 +fi diff --git a/.config/nvim/pack/tree/start/nui.nvim/tests/helpers/init.lua b/.config/nvim/pack/tree/start/nui.nvim/tests/helpers/init.lua new file mode 100644 index 0000000..e8a23fd --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/tests/helpers/init.lua @@ -0,0 +1,312 @@ +local function to_string(text) + if type(text) == "string" then + return text + end + + if type(text) == "table" then + if text.content then + return text:content() + end + + return text[1] + end + + error("unsupported text") +end + +local popup = {} + +local mod = {} + +mod.popup = popup + +function mod.eq(...) + return assert.are.same(...) +end + +function mod.approx(...) + return assert.are.near(...) +end + +function mod.neq(...) + return assert["not"].are.same(...) +end + +---@param fn fun(): nil +---@param error string +---@param is_plain boolean +function mod.errors(fn, error, is_plain) + assert.matches_error(fn, error, 1, is_plain) +end + +---@param keys string +---@param mode string +function mod.feedkeys(keys, mode) + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(keys, true, false, true), mode or "", true) +end + +---@param tbl table +---@param keys string[] +function mod.tbl_pick(tbl, keys) + if not keys or #keys == 0 then + return tbl + end + + local new_tbl = {} + for _, key in ipairs(keys) do + new_tbl[key] = tbl[key] + end + return new_tbl +end + +---@param tbl table +---@param keys string[] +function mod.tbl_omit(tbl, keys) + if not keys or #keys == 0 then + return tbl + end + + local new_tbl = vim.deepcopy(tbl) + for _, key in ipairs(keys) do + rawset(new_tbl, key, nil) + end + return new_tbl +end + +---@param bufnr number +---@param ns_id integer +---@param linenr integer (1-indexed) +---@param byte_start? integer (0-indexed) +---@param byte_end? integer (0-indexed, inclusive) +function mod.get_line_extmarks(bufnr, ns_id, linenr, byte_start, byte_end) + return vim.api.nvim_buf_get_extmarks( + bufnr, + ns_id, + { linenr - 1, byte_start or 0 }, + { linenr - 1, byte_end and byte_end + 1 or -1 }, + { details = true } + ) +end + +---@param bufnr number +---@param ns_id integer +---@param linenr integer (1-indexed) +---@param text string +---@return table[] +---@return { byte_start: integer, byte_end: integer } info (byte range: 0-indexed, inclusive) +function mod.get_text_extmarks(bufnr, ns_id, linenr, text) + local line = vim.api.nvim_buf_get_lines(bufnr, linenr - 1, linenr, false)[1] + + local byte_start = string.find(line, text) -- 1-indexed + byte_start = byte_start - 1 -- 0-indexed + local byte_end = byte_start + #text - 1 -- inclusive + + local extmarks = vim.api.nvim_buf_get_extmarks( + bufnr, + ns_id, + { linenr - 1, byte_start }, + { linenr - 1, byte_end }, + { details = true } + ) + + return extmarks, { byte_start = byte_start, byte_end = byte_end } +end + +---@param bufnr number +---@param lines string[] +---@param linenr_start? integer (1-indexed) +---@param linenr_end? integer (1-indexed, inclusive) +function mod.assert_buf_lines(bufnr, lines, linenr_start, linenr_end) + mod.eq(vim.api.nvim_buf_get_lines(bufnr, linenr_start and linenr_start - 1 or 0, linenr_end or -1, false), lines) +end + +---@param bufnr number +---@param options table +function mod.assert_buf_options(bufnr, options) + for name, value in pairs(options) do + mod.eq(vim.api.nvim_buf_get_option(bufnr, name), value) + end +end + +---@param winid number +---@param options table +function mod.assert_win_options(winid, options) + for name, value in pairs(options) do + mod.eq(vim.api.nvim_win_get_option(winid, name), value) + end +end + +---@param extmark table +---@param linenr number (1-indexed) +---@param text string +---@param hl_group string +function mod.assert_extmark(extmark, linenr, text, hl_group) + mod.eq(extmark[2], linenr - 1) + + if text then + local start_col = extmark[3] + mod.eq(extmark[4].end_col - start_col, #text) + end + + mod.eq(mod.tbl_pick(extmark[4], { "end_row", "hl_group" }), { + end_row = linenr - 1, + hl_group = hl_group, + }) +end + +---@param bufnr number +---@param ns_id integer +---@param linenr integer (1-indexed) +---@param text string +---@param hl_group string +function mod.assert_highlight(bufnr, ns_id, linenr, text, hl_group) + local extmarks, info = mod.get_text_extmarks(bufnr, ns_id, linenr, text) + + mod.eq(#extmarks, 1) + mod.eq(extmarks[1][3], info.byte_start) + mod.assert_extmark(extmarks[1], linenr, text, hl_group) +end + +---@param feature_name string +---@param desc string +---@param func fun(is_available: boolean):nil +function mod.describe_flipping_feature(feature_name, desc, func) + local initial_value = require("nui.utils")._.feature[feature_name] + + describe(string.format("(w/ %s) %s", feature_name, desc), function() + require("nui.utils")._.feature[feature_name] = true + func(true) + require("nui.utils")._.feature[feature_name] = initial_value + end) + + describe(string.format("(w/o %s) %s", feature_name, desc), function() + require("nui.utils")._.feature[feature_name] = false + func(false) + require("nui.utils")._.feature[feature_name] = initial_value + end) +end + +function popup.create_border_style_list() + return { "╭", "─", "╮", "│", "╯", "─", "╰", "│" } +end + +function popup.create_border_style_map() + return { + top_left = "╭", + top = "─", + top_right = "╮", + left = "│", + right = "│", + bottom_left = "╰", + bottom = "─", + bottom_right = "╯", + } +end + +function popup.create_border_style_map_with_tuple(hl_group) + local style = popup.create_border_style_map() + for k, v in pairs(style) do + style[k] = { v, hl_group .. "_" .. k } + end + return style +end + +function popup.create_border_style_map_with_nui_text(hl_group) + local Text = require("nui.text") + + local style = popup.create_border_style_map() + for k, v in pairs(style) do + style[k] = Text(v, hl_group .. "_" .. k) + end + + return style +end + +function popup.assert_border_lines(options, border_bufnr) + local size = { width = options.size.width, height = options.size.height } + -- `vim.tbl_islist` will be removed in the future + local islist = vim.islist or vim.tbl_islist + + local style = vim.deepcopy(options.border.style) + if islist(style) then + style = { + top_left = style[1], + top = style[2], + top_right = style[3], + left = style[8], + right = style[4], + bottom_left = style[7], + bottom = style[6], + bottom_right = style[5], + } + end + + local expected_lines = {} + table.insert( + expected_lines, + string.format( + "%s%s%s", + to_string(style.top_left), + string.rep(to_string(style.top), size.width), + to_string(style.top_right) + ) + ) + for _ = 1, size.height do + table.insert( + expected_lines, + string.format("%s%s%s", to_string(style.left), string.rep(" ", size.width), to_string(style.right)) + ) + end + table.insert( + expected_lines, + string.format( + "%s%s%s", + to_string(style.bottom_left), + string.rep(to_string(style.bottom), size.width), + to_string(style.bottom_right) + ) + ) + + mod.assert_buf_lines(border_bufnr, expected_lines) +end + +function popup.assert_border_highlight(options, border_bufnr, hl_group, no_hl_group_suffix) + local size = { width = options.size.width, height = options.size.height } + + for linenr = 1, size.height + 2 do + local is_top_line = linenr == 1 + local is_bottom_line = linenr == size.height + 2 + + local extmarks = mod.get_line_extmarks(border_bufnr, options.ns_id, linenr) + + mod.eq(#extmarks, (is_top_line or is_bottom_line) and 4 or 2) + + local function with_suffix(hl_group_name, suffix) + if no_hl_group_suffix then + return hl_group_name + end + return hl_group_name .. suffix + end + + mod.assert_extmark( + extmarks[1], + linenr, + nil, + with_suffix(hl_group, (is_top_line and "_top_left" or is_bottom_line and "_bottom_left" or "_left")) + ) + + if is_top_line or is_bottom_line then + mod.assert_extmark(extmarks[2], linenr, nil, with_suffix(hl_group, (is_top_line and "_top" or "_bottom"))) + mod.assert_extmark(extmarks[3], linenr, nil, with_suffix(hl_group, (is_top_line and "_top" or "_bottom"))) + end + + mod.assert_extmark( + extmarks[#extmarks], + linenr, + nil, + with_suffix(hl_group, (is_top_line and "_top_right" or is_bottom_line and "_bottom_right" or "_right")) + ) + end +end + +return mod diff --git a/.config/nvim/pack/tree/start/nui.nvim/tests/init.lua b/.config/nvim/pack/tree/start/nui.nvim/tests/init.lua new file mode 100644 index 0000000..10e559f --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/tests/init.lua @@ -0,0 +1,24 @@ +-- mimic startup option `--clean` +local function clean_startup() + for _, path in ipairs(vim.split(vim.o.runtimepath, ",")) do + if + string.find(path, vim.fn.expand("~/.config/nvim")) + or string.find(path, vim.fn.expand("~/.local/share/nvim/site")) + then + vim.opt.packpath:remove(path) + vim.opt.runtimepath:remove(path) + end + end +end + +clean_startup() + +local root_dir = vim.fn.fnamemodify(vim.trim(vim.fn.system("git rev-parse --show-toplevel")), ":p"):gsub("/$", "") + +package.path = string.format("%s;%s/?.lua;%s/?/init.lua", package.path, root_dir, root_dir) + +vim.opt.packpath:prepend(root_dir .. "/.tests/site") + +vim.cmd([[ + packadd plenary.nvim +]]) diff --git a/.config/nvim/pack/tree/start/nui.nvim/tests/nui/input/init_spec.lua b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/input/init_spec.lua new file mode 100644 index 0000000..6862788 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/input/init_spec.lua @@ -0,0 +1,285 @@ +pcall(require, "luacov") + +local Input = require("nui.input") +local Text = require("nui.text") +local h = require("tests.helpers") + +local eq, feedkeys = h.eq, h.feedkeys + +-- Input's functionalities are not testable using headless nvim. +-- Not sure what to do about it. + +describe("nui.input", function() + local parent_winid, parent_bufnr + local popup_options + local input + + before_each(function() + parent_winid = vim.api.nvim_get_current_win() + parent_bufnr = vim.api.nvim_get_current_buf() + + popup_options = { + relative = "win", + position = "50%", + size = 20, + } + end) + + after_each(function() + if input then + input:unmount() + input = nil + end + end) + + pending("o.prompt", function() + it("supports NuiText", function() + local prompt_text = "> " + local hl_group = "NuiInputTest" + + input = Input(popup_options, { + prompt = Text(prompt_text, hl_group), + }) + + input:mount() + vim.wait(100, function() end) + + h.assert_buf_lines(input.bufnr, { + prompt_text, + }) + + h.assert_highlight(input.bufnr, input.ns_id, 1, prompt_text, hl_group) + end) + end) + + describe("o.on_change", function() + it("works", function() + local done = false + local values = {} + + input = Input(popup_options, { + on_change = function(value) + values[#values + 1] = value + end, + on_close = function() + done = true + end, + }) + + input:mount() + vim.wait(100, function() end) + + feedkeys("aa", "x") -- append a + feedkeys("ab", "x") -- append b + feedkeys("ac", "x") -- append c + + vim.fn.wait(100, function() + return done + end) + + eq(values, { "a", "ab", "abc" }) + end) + end) + + describe("o.on_close", function() + it("is called on ", function() + local done = false + + input = Input(popup_options, { + on_close = function() + done = true + end, + }) + + input:mount() + vim.wait(100, function() end) + + feedkeys("i", "x") + + vim.fn.wait(2000, function() + return done + end) + + eq(done, true) + end) + + it("is called on unmount", function() + local done = false + + input = Input(popup_options, { + on_close = function() + done = true + end, + }) + + input:mount() + vim.wait(100, function() end) + + input:unmount() + + vim.fn.wait(200, function() + return done + end) + + eq(done, true) + end) + end) + + describe("cursor_position_patch", function() + local initial_cursor + + local function setup() + vim.api.nvim_buf_set_lines(parent_bufnr, 0, -1, false, { + "1 nui.nvim", + "2 nui.nvim", + "3 nui.nvim", + }) + initial_cursor = { 2, 4 } + vim.api.nvim_win_set_cursor(parent_winid, initial_cursor) + end + + it("works after submitting from insert mode", function() + setup() + + local done = false + input = Input(popup_options, { + on_submit = function() + done = true + end, + }) + + input:mount() + vim.wait(100, function() end) + + feedkeys("", "x") + + vim.fn.wait(1000, function() + return done + end) + + eq(done, true) + eq(vim.api.nvim_win_get_cursor(parent_winid), initial_cursor) + end) + + it("works after submitting from normal mode", function() + setup() + + local done = false + input = Input(popup_options, { + on_submit = function() + done = true + end, + }) + + input:mount() + vim.wait(100, function() end) + + feedkeys("", "x") + + vim.fn.wait(1000, function() + return done + end) + + eq(done, true) + eq(vim.api.nvim_win_get_cursor(parent_winid), initial_cursor) + end) + + it("works after closing from insert mode", function() + setup() + + local done = false + input = Input(popup_options, { + on_close = function() + done = true + end, + }) + + input:mount() + vim.wait(100, function() end) + + input:map("i", "", function() + input:unmount() + end, { nowait = true, noremap = true }) + + feedkeys("i", "x") + + vim.fn.wait(1000, function() + return done + end) + + eq(done, true) + eq(vim.api.nvim_win_get_cursor(parent_winid), initial_cursor) + end) + + it("works after closing from normal mode", function() + setup() + + local done = false + input = Input(popup_options, { + on_close = function() + done = true + end, + }) + + input:mount() + vim.wait(100, function() end) + + input:map("n", "", function() + input:unmount() + end, { nowait = true, noremap = true }) + + feedkeys("", "x") + + vim.fn.wait(1000, function() + return done + end) + + eq(done, true) + eq(vim.api.nvim_win_get_cursor(parent_winid), initial_cursor) + end) + end) + + describe("method :mount", function() + it("is idempotent", function() + input = Input(popup_options, {}) + + input:mount() + vim.wait(100, function() end) + + local bufnr, winid = input.bufnr, input.winid + + eq(type(bufnr), "number") + eq(type(winid), "number") + + input:mount() + + eq(bufnr, input.bufnr) + eq(winid, input.winid) + end) + end) + + describe("method :unmount", function() + it("is idempotent", function() + local done = 0 + + input = Input(popup_options, { + on_close = function() + done = done + 1 + end, + }) + + input:mount() + vim.wait(100, function() end) + + input:unmount() + input:unmount() + input:unmount() + + vim.fn.wait(200, function() + return done > 1 + end) + + eq(done, 1) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/nui.nvim/tests/nui/layout/init_spec.lua b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/layout/init_spec.lua new file mode 100644 index 0000000..fe9cbca --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/layout/init_spec.lua @@ -0,0 +1,2026 @@ +pcall(require, "luacov") + +local Layout = require("nui.layout") +local Popup = require("nui.popup") +local Split = require("nui.split") +local h = require("tests.helpers") +local spy = require("luassert.spy") + +local eq, tbl_pick = h.eq, h.tbl_pick + +local function create_popups(...) + local popups = {} + for _, popup_options in ipairs({ ... }) do + table.insert(popups, Popup(popup_options)) + end + return popups +end + +local function create_splits(...) + local splits = {} + for _, split_options in ipairs({ ... }) do + table.insert(splits, Split(split_options)) + end + return splits +end + +local function percent(number, percentage) + return math.floor(number * percentage / 100) +end + +local function get_assert_component(layout) + local layout_winid = layout.winid + assert(layout_winid, "missing layout.winid, forgot to mount it?") + + return function(component, expected) + eq(type(component.bufnr), "number") + eq(type(component.winid), "number") + + local win_config, border_win_config = + vim.api.nvim_win_get_config(component.winid), + component.border.winid and vim.api.nvim_win_get_config(component.border.winid) + if border_win_config then + eq(border_win_config.relative, "win") + eq(border_win_config.win, layout_winid) + + eq(win_config.relative, "win") + eq(win_config.win, component.border.winid) + else + eq(win_config.relative, "win") + eq(win_config.win, layout_winid) + end + + if border_win_config then + local border_row, border_col = border_win_config.row, border_win_config.col + eq(border_row, expected.position.row) + eq(border_col, expected.position.col) + + local row, col = win_config.row, win_config.col + eq(row, math.floor(component.border._.size_delta.width / 2 + 0.5)) + eq(col, math.floor(component.border._.size_delta.height / 2 + 0.5)) + else + local row, col = win_config.row, win_config.col + eq(row, expected.position.row) + eq(col, expected.position.col) + end + + local expected_width, expected_height = expected.size.width, expected.size.height + if component.border then + expected_width = expected_width - component.border._.size_delta.width + expected_height = expected_height - component.border._.size_delta.height + end + eq(vim.api.nvim_win_get_width(component.winid), expected_width) + eq(vim.api.nvim_win_get_height(component.winid), expected_height) + end +end + +describe("nui.layout", function() + local layout + + after_each(function() + if layout then + layout:unmount() + layout = nil + end + end) + + describe("param box", function() + it("throws if empty table", function() + local ok, err = pcall(function() + Layout({}, {}) + end) + + eq(ok, false) + eq(type(string.match(err, "unexpected empty box")), "string") + end) + + it("throws if empty box", function() + local ok, err = pcall(function() + Layout( + {}, + Layout.Box({ + Layout.Box({ + Layout.Box({}, { size = "50%" }), + Layout.Box({}, { size = "50%" }), + }, { size = "100%" }), + }) + ) + end) + + eq(ok, false) + eq(type(string.match(err, "unexpected empty box")), "string") + end) + + it("does not throw if non-empty", function() + local p1 = unpack(create_popups({})) + + local ok, err = pcall(function() + Layout( + { position = "50%", size = "100%" }, + Layout.Box({ + Layout.Box({ + Layout.Box(p1, { size = "50%" }), + Layout.Box({}, { size = "50%" }), + }, { size = "100%" }), + }) + ) + end) + + eq(ok, true) + eq(err, nil) + end) + end) + + describe("box", function() + it("throws if unknown component", function() + local unknown = require("nui.object")("Unknown")() + function unknown.mount() end + + local ok, err = pcall(function() + Layout.Box({ + Layout.Box(unknown, { size = "100%" }), + }) + end) + + eq(ok, false) + eq(type(string.match(err, "unsupported component")), "string") + end) + + it("requires child.size if child.grow is missing", function() + local p1, p2 = unpack(create_popups({}, {})) + + local ok, result = pcall(function() + Layout.Box({ + Layout.Box(p1, { size = "50%" }), + Layout.Box(p2, {}), + }) + end) + + eq(ok, false) + eq(type(string.match(result, "missing child.size")), "string") + end) + + it("does not require child.size if child.grow is present", function() + local p1, p2 = unpack(create_popups({}, {})) + + local ok = pcall(function() + Layout.Box({ + Layout.Box(p1, { size = "50%" }), + Layout.Box(p2, { grow = 1 }), + }) + end) + + eq(ok, true) + end) + + describe("size (table)", function() + it("missing height is set to 100% if dir=row", function() + local p1, p2 = unpack(create_popups({}, {})) + + local box = Layout.Box({ + Layout.Box(p1, { size = { width = "40%" } }), + Layout.Box(p2, { size = { width = "60%", height = "80%" } }), + }, { dir = "row" }) + + eq(box.box[1].size, { + width = "40%", + height = "100%", + }) + eq(box.box[2].size, { + width = "60%", + height = "80%", + }) + end) + + it("missing width is set to 100% if dir=col", function() + local p1, p2 = unpack(create_popups({}, {})) + + local box = Layout.Box({ + Layout.Box(p1, { size = { height = "40%" } }), + Layout.Box(p2, { size = { width = "60%", height = "80%" } }), + }, { dir = "col" }) + + eq(box.box[1].size, { + width = "100%", + height = "40%", + }) + eq(box.box[2].size, { + width = "60%", + height = "80%", + }) + end) + end) + + describe("size (percentage string)", function() + it("is set to width if dir=row", function() + local p1, p2 = unpack(create_popups({}, {})) + + local box = Layout.Box({ + Layout.Box(p1, { size = "40%" }), + Layout.Box(p2, { size = "60%" }), + }, { dir = "row" }) + + eq(box.box[1].size, { + width = "40%", + height = "100%", + }) + eq(box.box[2].size, { + width = "60%", + height = "100%", + }) + end) + + it("is set to height if dir=col", function() + local p1, p2 = unpack(create_popups({}, {})) + + local box = Layout.Box({ + Layout.Box(p1, { size = "40%" }), + Layout.Box(p2, { size = "60%" }), + }, { dir = "col" }) + + eq(box.box[1].size, { + width = "100%", + height = "40%", + }) + eq(box.box[2].size, { + width = "100%", + height = "60%", + }) + end) + end) + end) + + describe("[float]", function() + describe("o.size", function() + local box + + before_each(function() + local p1 = unpack(create_popups({})) + box = Layout.Box({ Layout.Box(p1, { size = "100%" }) }) + end) + + local function assert_size(size) + local win_config = vim.api.nvim_win_get_config(layout.winid) + + eq(tbl_pick(win_config, { "width", "height" }), { + width = math.floor(size.width), + height = math.floor(size.height), + }) + end + + it("supports number", function() + local size = 20 + + layout = Layout({ + position = "50%", + size = size, + }, box) + + layout:mount() + + assert_size({ width = size, height = size }) + end) + + it("supports percentage string", function() + local percentage = 50 + + layout = Layout({ + position = "50%", + size = string.format("%s%%", percentage), + }, box) + + local winid = vim.api.nvim_get_current_win() + local win_width = vim.api.nvim_win_get_width(winid) + local win_height = vim.api.nvim_win_get_height(winid) + + layout:mount() + + assert_size({ + width = win_width * percentage / 100, + height = win_height * percentage / 100, + }) + end) + + it("supports table", function() + local width = 10 + local height_percentage = 50 + + layout = Layout({ + position = "50%", + size = { + width = width, + height = string.format("%s%%", height_percentage), + }, + }, box) + + local winid = vim.api.nvim_get_current_win() + local win_height = vim.api.nvim_win_get_height(winid) + + layout:mount() + + assert_size({ + width = width, + height = win_height * height_percentage / 100, + }) + end) + end) + + describe("o.position", function() + local box + + before_each(function() + local p1 = unpack(create_popups({})) + box = Layout.Box({ Layout.Box(p1, { size = "100%" }) }) + end) + + local function assert_position(position) + local row, col = unpack(vim.api.nvim_win_get_position(layout.winid)) + + eq(row, math.floor(position.row)) + eq(col, math.floor(position.col)) + end + + it("supports number", function() + local position = 5 + + layout = Layout({ + position = position, + size = 10, + }, box) + + layout:mount() + + assert_position({ row = position, col = position }) + end) + + it("supports percentage string", function() + local size = 10 + local percentage = 50 + + layout = Layout({ + position = string.format("%s%%", percentage), + size = size, + }, box) + + layout:mount() + + local winid = vim.api.nvim_get_current_win() + local win_width = vim.api.nvim_win_get_width(winid) + local win_height = vim.api.nvim_win_get_height(winid) + + assert_position({ + row = (win_height - size) * percentage / 100, + col = (win_width - size) * percentage / 100, + }) + end) + + it("supports table", function() + local size = 10 + local row = 5 + local col_percentage = 50 + + layout = Layout({ + position = { + row = row, + col = string.format("%s%%", col_percentage), + }, + size = size, + }, box) + + layout:mount() + + local winid = vim.api.nvim_get_current_win() + local win_width = vim.api.nvim_win_get_width(winid) + + assert_position({ + row = row, + col = (win_width - size) * col_percentage / 100, + }) + end) + end) + + describe("method :mount", function() + it("mounts all components", function() + local p1, p2 = unpack(create_popups({}, {})) + + local p1_mount = spy.on(p1, "mount") + local p2_mount = spy.on(p2, "mount") + + layout = Layout( + { + position = "50%", + size = { + height = 20, + width = 100, + }, + }, + Layout.Box({ + Layout.Box(p1, { size = "50%" }), + Layout.Box(p2, { size = "50%" }), + }) + ) + + layout:mount() + + eq(type(layout.bufnr), "number") + eq(type(layout.winid), "number") + + assert.spy(p1_mount).was_called() + assert.spy(p2_mount).was_called() + end) + + it("is idempotent", function() + local p1, p2 = unpack(create_popups({}, {})) + + local p1_mount = spy.on(p1, "mount") + local p2_mount = spy.on(p2, "mount") + + layout = Layout( + { + position = "50%", + size = 20, + }, + Layout.Box({ + Layout.Box(p1, { size = "50%" }), + Layout.Box(p2, { size = "50%" }), + }) + ) + + layout:mount() + + assert.spy(p1_mount).was_called(1) + assert.spy(p2_mount).was_called(1) + + layout:mount() + + assert.spy(p1_mount).was_called(1) + assert.spy(p2_mount).was_called(1) + end) + + it("supports container component", function() + local p1, p2 = unpack(create_popups({}, {})) + + local split = Split({ + relative = "editor", + position = "bottom", + size = 10, + }) + + local split_mount = spy.on(split, "mount") + + layout = Layout( + split, + Layout.Box({ + Layout.Box(p1, { size = "50%" }), + Layout.Box(p2, { size = "50%" }), + }) + ) + + layout:mount() + + assert.spy(split_mount).was_called(1) + + local win_config = vim.api.nvim_win_get_config(layout.winid) + eq(win_config.relative, "win") + eq(win_config.row, 0) + eq(win_config.col, 0) + eq(win_config.width, vim.o.columns) + eq(win_config.height, 10) + + split:unmount() + end) + + it("throws if missing config 'size'", function() + local p1 = unpack(create_popups({}, {})) + + local ok, result = pcall(function() + layout = Layout({}, { Layout.Box(p1, { size = "100%" }) }) + end) + + eq(ok, false) + eq(type(string.match(result, "missing layout config: size")), "string") + end) + + it("throws if missing config 'position'", function() + local p1 = unpack(create_popups({}, {})) + + local ok, result = pcall(function() + layout = Layout({ + size = "50%", + }, { Layout.Box(p1, { size = "100%" }) }) + end) + + eq(ok, false) + eq(type(string.match(result, "missing layout config: position")), "string") + end) + end) + + h.describe_flipping_feature("lua_autocmd", "method :unmount", function() + it("is called if any popup is unmounted", function() + local p1, p2 = unpack(create_popups({}, {}, {})) + + layout = Layout( + { + position = "50%", + size = 10, + }, + Layout.Box({ + Layout.Box(p1, { size = "50%" }), + Layout.Box(p2, { size = "50%" }), + }) + ) + + local layout_unmount = spy.on(layout, "unmount") + + layout:mount() + + p2:unmount() + + vim.wait(100, function() + return not layout._.mounted + end, 10) + + assert.spy(layout_unmount).was_called() + end) + + it("is called if any popup is quitted", function() + local p1, p2 = unpack(create_popups({}, {})) + + layout = Layout( + { + position = "50%", + size = 10, + }, + Layout.Box({ + Layout.Box(p1, { size = "50%" }), + Layout.Box(p2, { size = "50%" }), + }) + ) + + local layout_unmount = spy.on(layout, "unmount") + + layout:mount() + + vim.api.nvim_buf_call(p2.bufnr, function() + vim.cmd([[quit]]) + end) + + vim.wait(100, function() + return not layout._.mounted + end, 10) + + assert.spy(layout_unmount).was_called() + end) + end) + + h.describe_flipping_feature("lua_autocmd", "method :hide", function() + it("does nothing if not mounted", function() + local p1 = unpack(create_popups({})) + + local p1_hide = spy.on(p1, "hide") + + layout = Layout( + { + position = "50%", + size = 10, + }, + Layout.Box({ + Layout.Box(p1, { size = "100%" }), + }) + ) + + layout:hide() + + assert.spy(p1_hide).was_not_called() + end) + + it("hides all components", function() + local p1, p2, p3 = unpack(create_popups({}, {}, {})) + + local p1_hide = spy.on(p1, "hide") + local p2_hide = spy.on(p2, "hide") + local p3_hide = spy.on(p3, "hide") + + layout = Layout( + { + position = "50%", + size = 10, + }, + Layout.Box({ + Layout.Box(p1, { size = "50%" }), + Layout.Box({ + Layout.Box(p2, { size = "50%" }), + Layout.Box({ + Layout.Box(p3, { size = "100%" }), + }, { size = "50%" }), + }, { size = "50%" }), + }) + ) + + layout:mount() + + eq(type(layout.winid), "number") + + layout:hide() + + eq(type(layout.winid), "nil") + + assert.spy(p1_hide).was_called() + assert.spy(p2_hide).was_called() + assert.spy(p3_hide).was_called() + end) + + it("is called if any popup is hidden", function() + local p1, p2, p3 = unpack(create_popups({}, {}, {})) + + layout = Layout( + { + position = "50%", + size = 10, + }, + Layout.Box({ + Layout.Box(p1, { size = "50%" }), + Layout.Box({ + Layout.Box(p2, { size = "50%" }), + Layout.Box({ + Layout.Box(p3, { size = "100%" }), + }, { size = "50%" }), + }, { size = "50%" }), + }) + ) + + local layout_hide = spy.on(layout, "hide") + + layout:mount() + + p2:hide() + + assert.spy(layout_hide).was_called() + end) + end) + + describe("method :show", function() + it("mounts if not mounted", function() + local p1 = unpack(create_popups({})) + + local p1_mount = spy.on(p1, "mount") + local p1_show = spy.on(p1, "show") + + layout = Layout( + { + position = "50%", + size = 10, + }, + Layout.Box({ + Layout.Box(p1, { size = "100%" }), + }) + ) + + layout:hide() + layout:show() + + assert.spy(p1_mount).was_called() + assert.spy(p1_show).was_not_called() + end) + + it("shows all components", function() + local p1, p2, p3 = unpack(create_popups({}, {}, {})) + + local p1_show = spy.on(p1, "show") + local p2_show = spy.on(p2, "show") + local p3_show = spy.on(p3, "show") + + layout = Layout( + { + position = "50%", + size = 10, + }, + Layout.Box({ + Layout.Box(p1, { size = "50%" }), + Layout.Box({ + Layout.Box(p2, { size = "50%" }), + Layout.Box({ + Layout.Box(p3, { size = "100%" }), + }, { size = "50%" }), + }, { size = "50%" }), + }) + ) + + layout:mount() + + layout:hide() + layout:show() + + eq(type(layout.winid), "number") + + assert.spy(p1_show).was_called() + assert.spy(p2_show).was_called() + assert.spy(p3_show).was_called() + end) + end) + + describe("method :update", function() + local winid, win_width, win_height + local p1, p2, p3, p4 + local assert_component + + before_each(function() + winid = vim.api.nvim_get_current_win() + win_width = vim.api.nvim_win_get_width(winid) + win_height = vim.api.nvim_win_get_height(winid) + + p1, p2, p3, p4 = unpack(create_popups({ + border = { style = "single" }, + }, {}, { + border = { style = "rounded", text = {} }, + }, {})) + end) + + local function get_initial_layout(config) + return Layout( + config, + Layout.Box({ + Layout.Box(p1, { size = "20%" }), + Layout.Box({ + Layout.Box(p3, { size = "50%" }), + Layout.Box(p4, { size = "50%" }), + }, { dir = "col", size = "60%" }), + Layout.Box(p2, { size = "20%" }), + }, { dir = "row" }) + ) + end + + local function assert_layout_config(config) + local relative, position, size = config.relative, config.position, config.size + + local win_config = vim.api.nvim_win_get_config(layout.winid) + eq(win_config.anchor, config.anchor or "NW") + eq(win_config.relative, relative.type) + eq(win_config.win, relative.winid) + + local row, col = unpack(vim.api.nvim_win_get_position(layout.winid)) + eq(row, position.row) + eq(col, position.col) + + eq(vim.api.nvim_win_get_width(layout.winid), size.width) + eq(vim.api.nvim_win_get_height(layout.winid), size.height) + end + + local function assert_initial_layout_components() + local size = { + width = vim.api.nvim_win_get_width(layout.winid), + height = vim.api.nvim_win_get_height(layout.winid), + } + + assert_component(p1, { + position = { + row = 0, + col = 0, + }, + size = { + width = percent(size.width, 20), + height = size.height, + }, + }) + + assert_component(p3, { + position = { + row = 0, + col = percent(size.width, 20), + }, + size = { + width = percent(size.width, 60), + height = percent(size.height, 50), + }, + }) + + assert_component(p4, { + position = { + row = percent(size.height, 50), + col = percent(size.width, 20), + }, + size = { + width = percent(size.width, 60), + height = percent(size.height, 50), + }, + }) + + assert_component(p2, { + position = { + row = 0, + col = percent(size.width, 20) + percent(size.width, 60), + }, + size = { + width = percent(size.width, 20), + height = size.height, + }, + }) + end + + it("processes layout correctly on mount", function() + local layout_update_spy = spy.on(Layout, "update") + + layout = get_initial_layout({ position = 0, size = "100%" }) + + layout:mount() + + layout_update_spy:revert() + assert.spy(layout_update_spy).was_called(1) + + local expected_layout_config = { + relative = { + type = "win", + winid = winid, + }, + position = { + row = 0, + col = 0, + }, + size = { + width = win_width, + height = win_height, + }, + } + + assert_layout_config(expected_layout_config) + + assert_component = get_assert_component(layout) + + assert_initial_layout_components() + end) + + it("can update layout win_config w/o rearranging boxes", function() + layout = get_initial_layout({ position = 0, size = "100%" }) + + layout:mount() + + layout:update({ + position = { + row = 2, + col = 4, + }, + size = "80%", + }) + + local expected_layout_config = { + relative = { + type = "win", + winid = winid, + }, + position = { + row = 2, + col = 4, + }, + size = { + width = percent(win_width, 80), + height = percent(win_height, 80), + }, + } + + assert_layout_config(expected_layout_config) + + assert_component = get_assert_component(layout) + + assert_initial_layout_components() + end) + + it("can rearrange boxes w/o changing layout win_config", function() + layout = get_initial_layout({ position = 0, size = "100%" }) + + layout:mount() + + layout:update(Layout.Box({ + Layout.Box(p2, { size = "30%" }), + Layout.Box({ + Layout.Box(p4, { size = "40%" }), + Layout.Box(p3, { size = "60%" }), + }, { dir = "row", size = "30%" }), + Layout.Box(p1, { size = "40%" }), + }, { dir = "col" })) + + local expected_layout_config = { + relative = { + type = "win", + winid = winid, + }, + position = { + row = 0, + col = 0, + }, + size = { + width = win_width, + height = win_height, + }, + } + + assert_layout_config(expected_layout_config) + + assert_component = get_assert_component(layout) + + assert_component(p2, { + position = { + row = 0, + col = 0, + }, + size = { + width = win_width, + height = percent(win_height, 30), + }, + }) + + assert_component(p4, { + position = { + row = percent(win_height, 30), + col = 0, + }, + size = { + width = percent(win_width, 40), + height = percent(win_height, 30), + }, + }) + + assert_component(p3, { + position = { + row = percent(win_height, 30), + col = percent(win_width, 40), + }, + size = { + width = percent(win_width, 60), + height = percent(win_height, 30), + }, + }) + + assert_component(p1, { + position = { + row = percent(win_height, 30) + percent(win_height, 30), + col = 0, + }, + size = { + width = win_width, + height = percent(win_height, 40), + }, + }) + end) + + it("refreshes layout if container size changes", function() + local popup = Popup({ + position = 0, + size = "100%", + }) + + popup:mount() + + layout = get_initial_layout({ + relative = { + type = "win", + winid = popup.winid, + }, + position = 0, + size = "80%", + }) + + layout:mount() + + local expected_layout_config = { + relative = { + type = "win", + winid = popup.winid, + }, + position = { + row = 0, + col = 0, + }, + size = { + width = percent(win_width, 80), + height = percent(win_height, 80), + }, + } + + assert_layout_config(expected_layout_config) + + assert_component = get_assert_component(layout) + + assert_initial_layout_components() + + popup:update_layout({ + size = "80%", + }) + + layout:update() + + expected_layout_config.size = { + width = percent(percent(win_width, 80), 80), + height = percent(percent(win_height, 80), 80), + } + + assert_layout_config(expected_layout_config) + + assert_initial_layout_components() + end) + + it("supports child with child.grow", function() + layout = get_initial_layout({ position = 0, size = "100%" }) + + layout:mount() + + layout:update(Layout.Box({ + Layout.Box(p1, { size = "20%" }), + Layout.Box({ + Layout.Box({}, { size = 4 }), + Layout.Box(p3, { grow = 1 }), + Layout.Box({}, { size = 8 }), + Layout.Box(p4, { grow = 1 }), + }, { dir = "col", size = "60%" }), + Layout.Box(p2, { grow = 1 }), + }, { dir = "row" })) + + local expected_layout_config = { + relative = { + type = "win", + winid = winid, + }, + position = { + row = 0, + col = 0, + }, + size = { + width = win_width, + height = win_height, + }, + } + + assert_layout_config(expected_layout_config) + + assert_component = get_assert_component(layout) + + assert_component(p1, { + position = { + row = 0, + col = 0, + }, + size = { + width = percent(win_width, 20), + height = win_height, + }, + }) + + assert_component(p3, { + position = { + row = 4, + col = percent(win_width, 20), + }, + size = { + width = percent(win_width, 60), + height = percent(win_height - 4 - 8, 100 / 2), + }, + }) + + assert_component(p4, { + position = { + row = 4 + 8 + percent(win_height - 4 - 8, 100 / 2), + col = percent(win_width, 20), + }, + size = { + width = percent(win_width, 60), + height = percent(win_height - 4 - 8, 100 / 2), + }, + }) + + assert_component(p2, { + position = { + row = 0, + col = percent(win_width, 20) + percent(win_width, 60), + }, + size = { + width = percent(win_width, 100 - 20 - 60), + height = win_height, + }, + }) + end) + + it("handles if child.grow results in height <= 0", function() + layout = get_initial_layout({ position = 0, size = "100%" }) + + layout:update(Layout.Box({ + Layout.Box(p1, { size = win_height }), + Layout.Box(p2, { grow = 1 }), + }, { dir = "col" })) + + layout:mount() + + local expected_layout_config = { + relative = { + type = "win", + winid = winid, + }, + position = { + row = 0, + col = 0, + }, + size = { + width = win_width, + height = win_height, + }, + } + + assert_layout_config(expected_layout_config) + + assert_component = get_assert_component(layout) + + assert_component(p1, { + position = { + row = 0, + col = 0, + }, + size = { + width = win_width, + height = win_height, + }, + }) + + assert_component(p2, { + position = { + row = win_height, + col = 0, + }, + size = { + width = win_width, + height = 1, + }, + }) + end) + + it("can change boxes", function() + layout = Layout( + { position = 0, size = "100%" }, + Layout.Box({ + Layout.Box(p1, { size = "40%" }), + Layout.Box(p2, { size = "60%" }), + }, { dir = "col" }) + ) + + layout:mount() + + assert_component = get_assert_component(layout) + + assert_component(p1, { + position = { + row = 0, + col = 0, + }, + size = { + width = win_width, + height = percent(win_height, 40), + }, + }) + + assert_component(p2, { + position = { + row = percent(win_height, 40), + col = 0, + }, + size = { + width = win_width, + height = percent(win_height, 60), + }, + }) + + layout:update(Layout.Box({ + Layout.Box({ + Layout.Box(p1, { size = "40%" }), + Layout.Box(p2, { size = "60%" }), + }, { dir = "col", size = "60%" }), + Layout.Box(p3, { size = "40%" }), + }, { dir = "row" })) + + assert_component = get_assert_component(layout) + + assert_component(p1, { + position = { + row = 0, + col = 0, + }, + size = { + width = percent(win_width, 60), + height = percent(win_height, 40), + }, + }) + + assert_component(p2, { + position = { + row = percent(win_height, 40), + col = 0, + }, + size = { + width = percent(win_width, 60), + height = percent(win_height, 60), + }, + }) + + assert_component(p3, { + position = { + row = 0, + col = percent(win_width, 60), + }, + size = { + width = percent(win_width, 40), + height = win_height, + }, + }) + + layout:update(Layout.Box({ + Layout.Box({ + Layout.Box(p1, { size = "40%" }), + Layout.Box(p2, { size = "60%" }), + }, { dir = "col", size = "60%" }), + Layout.Box(p4, { size = "40%" }), + }, { dir = "row" })) + + assert_component(p4, { + position = { + row = 0, + col = percent(win_width, 60), + }, + size = { + width = percent(win_width, 40), + height = win_height, + }, + }) + + eq(p3.winid, nil) + + layout:update(Layout.Box({ + Layout.Box(p3, { size = "40%" }), + Layout.Box(p4, { size = "60%" }), + }, { dir = "col" })) + + eq(p1.winid, nil) + eq(p2.winid, nil) + + assert_component(p3, { + position = { + row = 0, + col = 0, + }, + size = { + width = win_width, + height = percent(win_height, 40), + }, + }) + + assert_component(p4, { + position = { + row = percent(win_height, 40), + col = 0, + }, + size = { + width = win_width, + height = percent(win_height, 60), + }, + }) + end) + + it("positions popup with complex border correctly", function() + p1 = unpack(create_popups({ + border = { + style = "single", + text = { + top = "text", + }, + padding = { 1 }, + }, + })) + + layout = Layout( + { position = 0, size = "100%" }, + Layout.Box({ + Layout.Box(p1, { size = "100%" }), + }, { dir = "col" }) + ) + + layout:mount() + + assert_component = get_assert_component(layout) + + assert_component(p1, { + position = { + row = 0, + col = 0, + }, + size = { + width = win_width, + height = percent(win_height, 100), + }, + }) + end) + + it("can change anchor", function() + layout = Layout({ + position = 0, + size = 10, + }, { + Layout.Box(p1, { size = "100%" }), + }) + + layout:mount() + + layout:update({ anchor = "SW" }) + + assert_layout_config({ + anchor = "SW", + relative = { type = "win", winid = winid }, + position = { row = 0, col = 0 }, + size = { height = 10, width = 10 }, + }) + end) + + it("can handle col[box,row[box,col[box,box]]]", function() + layout = get_initial_layout({ position = 0, size = "100%" }) + + layout:mount() + + layout:update(Layout.Box({ + Layout.Box(p1, { size = "50%" }), + Layout.Box({ + Layout.Box(p2, { size = "50%" }), + Layout.Box({ + Layout.Box(p3, { size = "50%" }), + Layout.Box(p4, { size = "50%" }), + }, { dir = "col", size = "50%" }), + }, { dir = "row", size = "50%" }), + }, { dir = "col" })) + + assert_component = get_assert_component(layout) + + assert_component(p1, { + position = { + row = 0, + col = 0, + }, + size = { + width = win_width, + height = percent(win_height, 50), + }, + }) + + assert_component(p2, { + position = { + row = percent(win_height, 50), + col = 0, + }, + size = { + width = percent(win_width, 50), + height = percent(win_height, 50), + }, + }) + + assert_component(p3, { + position = { + row = percent(win_height, 50), + col = percent(win_width, 50), + }, + size = { + width = percent(win_width, 50), + height = percent(percent(win_height, 50), 50), + }, + }) + + assert_component(p4, { + position = { + row = percent(win_height, 50 + 25), + col = percent(win_width, 50), + }, + size = { + width = percent(win_width, 50), + height = percent(percent(win_height, 50), 50), + }, + }) + end) + end) + end) + + describe("[split]", function() + local function assert_size(winid, expected, tolerance) + h.approx(vim.api.nvim_win_get_width(winid), expected.width, tolerance or 0) + h.approx(vim.api.nvim_win_get_height(winid), expected.height, tolerance or 0) + end + + describe("method :mount", function() + it("mounts all components", function() + local s1, s2, s3 = unpack(create_splits({}, {}, {})) + + local s1_mount = spy.on(s1, "mount") + local s2_mount = spy.on(s2, "mount") + local s3_mount = spy.on(s3, "mount") + + layout = Layout( + { + position = "bottom", + size = 20, + }, + Layout.Box({ + Layout.Box(s1, { size = "50%" }), + Layout.Box({ + Layout.Box(s2, { size = "50%" }), + Layout.Box({ + Layout.Box(s3, { size = "100%" }), + }, { size = "50%" }), + }, { size = "50%" }), + }) + ) + + layout:mount() + + eq(type(layout.bufnr), "nil") + eq(type(layout.winid), "nil") + + assert.spy(s1_mount).was_called() + assert.spy(s2_mount).was_called() + assert.spy(s3_mount).was_called() + end) + + it("is idempotent", function() + local s1, s2 = unpack(create_splits({}, {})) + + local s1_mount = spy.on(s1, "mount") + local s2_mount = spy.on(s2, "mount") + + layout = Layout( + { + position = "bottom", + size = 20, + }, + Layout.Box({ + Layout.Box(s1, { size = "50%" }), + Layout.Box(s2, { size = "50%" }), + }) + ) + + layout:mount() + + assert.spy(s1_mount).was_called(1) + assert.spy(s2_mount).was_called(1) + + layout:mount() + + assert.spy(s1_mount).was_called(1) + assert.spy(s2_mount).was_called(1) + end) + + it("mounts with correct layout", function() + local winid = vim.api.nvim_get_current_win() + + local s1, s2, s3, s4, s5 = unpack(create_splits({}, {}, {}, {}, {})) + + layout = Layout( + { + position = "bottom", + size = 20, + }, + Layout.Box({ + Layout.Box(s1, { size = "100%" }), + Layout.Box({ + Layout.Box({ + Layout.Box(s2, { size = "100%" }), + }, { dir = "col", size = "60%" }), + Layout.Box({ + Layout.Box(s3, { size = "40%" }), + Layout.Box(s4, { size = "60%" }), + }, { dir = "row", size = "40%" }), + Layout.Box({}, { size = "0%" }), + }, { dir = "col", size = "40%" }), + Layout.Box(s5, { size = "35%" }), + }) + ) + + layout:mount() + + eq(vim.fn.winlayout(), { + "col", + { + { "leaf", winid }, + { + "row", + { + { "leaf", s1.winid }, + { + "col", + { + { "leaf", s2.winid }, + { + "row", + { + { "leaf", s3.winid }, + { "leaf", s4.winid }, + }, + }, + }, + }, + { "leaf", s5.winid }, + }, + }, + }, + }) + end) + + it("mounts with acceptable sizes", function() + local winid = vim.api.nvim_get_current_win() + local base_size = { + width = vim.api.nvim_win_get_width(winid), + height = vim.api.nvim_win_get_height(winid), + } + + local s1, s2, s3, s4, s5 = unpack(create_splits({}, {}, {}, {}, {})) + + layout = Layout( + { + position = "bottom", + size = 20, + }, + Layout.Box({ + Layout.Box(s1, { size = "25%" }), + Layout.Box({ + Layout.Box({ + Layout.Box(s2, { size = "100%" }), + }, { dir = "col", size = "60%" }), + Layout.Box({ + Layout.Box(s3, { size = "40%" }), + Layout.Box(s4, { size = "60%" }), + }, { dir = "row", size = "40%" }), + Layout.Box({}, { size = "0%" }), + }, { dir = "col", size = "40%" }), + Layout.Box(s5, { size = "35%" }), + }) + ) + + layout:mount() + + assert_size(s1.winid, { + width = percent(base_size.width, 25), + height = percent(20, 100), + }, 2) + + assert_size(s2.winid, { + width = percent(base_size.width, 40), + height = percent(20, 60), + }, 1) + + assert_size(s3.winid, { + width = percent(percent(base_size.width, 40), 40), + height = percent(20, 40), + }, 1) + + assert_size(s4.winid, { + width = percent(percent(base_size.width, 40), 60), + height = percent(20, 40), + }, 1) + + assert_size(s5.winid, { + width = percent(base_size.width, 35), + height = percent(20, 100), + }) + end) + end) + + describe("method :unmount", function() + it("unmounts all components", function() + local s1, s2, s3 = unpack(create_splits({}, {}, {})) + + local s1_unmount = spy.on(s1, "unmount") + local s2_unmount = spy.on(s2, "unmount") + local s3_unmount = spy.on(s3, "unmount") + + layout = Layout( + { + position = "bottom", + size = 20, + }, + Layout.Box({ + Layout.Box(s1, { size = "50%" }), + Layout.Box({ + Layout.Box(s2, { size = "50%" }), + Layout.Box({ + Layout.Box(s3, { size = "100%" }), + }, { size = "50%" }), + }, { size = "50%" }), + }) + ) + + layout:mount() + layout:unmount() + + assert.spy(s1_unmount).was_called() + assert.spy(s2_unmount).was_called() + assert.spy(s3_unmount).was_called() + end) + + it("is called if any split is unmounted", function() + local s1, s2, s3 = unpack(create_splits({}, {}, {})) + + layout = Layout( + { + position = "bottom", + size = 20, + }, + Layout.Box({ + Layout.Box(s1, { size = "50%" }), + Layout.Box({ + Layout.Box(s2, { size = "50%" }), + Layout.Box({ + Layout.Box(s3, { size = "100%" }), + }, { size = "50%" }), + }, { size = "50%" }), + }) + ) + + local layout_unmount = spy.on(layout, "unmount") + + layout:mount() + + s2:unmount() + + vim.wait(100, function() + return not layout._.mounted + end, 10) + + assert.spy(layout_unmount).was_called() + end) + + it("is called if any split is quitted", function() + local s1, s2, s3 = unpack(create_splits({}, {}, {})) + + layout = Layout( + { + position = "bottom", + size = 20, + }, + Layout.Box({ + Layout.Box(s1, { size = "50%" }), + Layout.Box({ + Layout.Box(s2, { size = "50%" }), + Layout.Box({ + Layout.Box(s3, { size = "100%" }), + }, { size = "50%" }), + }, { size = "50%" }), + }) + ) + + local layout_unmount = spy.on(layout, "unmount") + + layout:mount() + + vim.api.nvim_buf_call(s2.bufnr, function() + vim.cmd([[quit]]) + end) + + vim.wait(100, function() + return not layout._.mounted + end, 10) + + assert.spy(layout_unmount).was_called() + end) + end) + + describe("method :hide", function() + it("does nothing if not mounted", function() + local s1 = unpack(create_splits({})) + + local s1_hide = spy.on(s1, "hide") + + layout = Layout( + { + position = "bottom", + size = 20, + }, + Layout.Box({ + Layout.Box(s1, { size = "100%" }), + }) + ) + + layout:hide() + + assert.spy(s1_hide).was_not_called() + end) + + it("hides all components", function() + local s1, s2, s3 = unpack(create_splits({}, {}, {})) + + local s1_hide = spy.on(s1, "hide") + local s2_hide = spy.on(s2, "hide") + local s3_hide = spy.on(s3, "hide") + + layout = Layout( + { + position = "bottom", + size = 20, + }, + Layout.Box({ + Layout.Box(s1, { size = "50%" }), + Layout.Box({ + Layout.Box(s2, { size = "50%" }), + Layout.Box({ + Layout.Box(s3, { size = "100%" }), + }, { size = "50%" }), + }, { size = "50%" }), + }) + ) + + layout:mount() + + layout:hide() + + assert.spy(s1_hide).was_called() + assert.spy(s2_hide).was_called() + assert.spy(s3_hide).was_called() + end) + + it("is called if any split is hidden", function() + local s1, s2, s3 = unpack(create_splits({}, {}, {})) + + layout = Layout( + { + position = "bottom", + size = 20, + }, + Layout.Box({ + Layout.Box(s1, { size = "50%" }), + Layout.Box({ + Layout.Box(s2, { size = "50%" }), + Layout.Box({ + Layout.Box(s3, { size = "100%" }), + }, { size = "50%" }), + }, { size = "50%" }), + }) + ) + + local layout_hide = spy.on(layout, "hide") + + layout:mount() + + s2:hide() + + assert.spy(layout_hide).was_called() + end) + end) + + describe("method :show", function() + it("mounts if not mounted", function() + local s1 = unpack(create_splits({})) + + local s1_mount = spy.on(s1, "mount") + local s1_show = spy.on(s1, "show") + + layout = Layout( + { + position = "bottom", + size = 20, + }, + Layout.Box({ + Layout.Box(s1, { size = "100%" }), + }) + ) + + layout:hide() + layout:show() + + assert.spy(s1_mount).was_called() + assert.spy(s1_show).was_not_called() + end) + + it("shows all components", function() + local s1, s2, s3 = unpack(create_splits({}, {}, {})) + + local s1_show = spy.on(s1, "show") + local s2_show = spy.on(s2, "show") + local s3_show = spy.on(s3, "show") + + layout = Layout( + { + position = "bottom", + size = 20, + }, + Layout.Box({ + Layout.Box(s1, { size = "50%" }), + Layout.Box({ + Layout.Box(s2, { size = "50%" }), + Layout.Box({ + Layout.Box(s3, { size = "100%" }), + }, { size = "50%" }), + }, { size = "50%" }), + }) + ) + + layout:mount() + + layout:hide() + layout:show() + + assert.spy(s1_show).was_called() + assert.spy(s2_show).was_called() + assert.spy(s3_show).was_called() + end) + end) + + describe("method :update", function() + local winid, base_size + local s1, s2, s3, s4 + + before_each(function() + winid = vim.api.nvim_get_current_win() + base_size = { + width = vim.api.nvim_win_get_width(winid), + height = vim.api.nvim_win_get_height(winid), + } + + s1, s2, s3, s4 = unpack(create_splits({}, {}, {}, {})) + end) + + local function get_initial_layout(config) + return Layout( + config, + Layout.Box({ + Layout.Box(s1, { size = "20%" }), + Layout.Box({ + Layout.Box(s2, { size = "40%" }), + Layout.Box(s3, { size = "60%" }), + }, { dir = "col", size = "50%" }), + Layout.Box(s4, { size = "30%" }), + }, { dir = "row" }) + ) + end + + it("can update layout win_config w/o rearranging boxes", function() + layout = get_initial_layout({ + position = "bottom", + size = 10, + }) + + layout:mount() + + assert_size(s1.winid, { + width = percent(base_size.width, 20), + height = percent(10, 100), + }, 2) + + assert_size(s2.winid, { + width = percent(base_size.width, 50), + height = percent(10, 40), + }, 1) + + assert_size(s3.winid, { + width = percent(base_size.width, 50), + height = percent(10, 60), + }) + + assert_size(s4.winid, { + width = percent(base_size.width, 30), + height = percent(10, 100), + }) + + layout:update({ size = 20 }) + + assert_size(s1.winid, { + width = percent(base_size.width, 20), + height = percent(20, 100), + }, 2) + + assert_size(s2.winid, { + width = percent(base_size.width, 50), + height = percent(20, 40), + }, 2) + + assert_size(s3.winid, { + width = percent(base_size.width, 50), + height = percent(20, 60), + }, 2) + + assert_size(s4.winid, { + width = percent(base_size.width, 30), + height = percent(20, 100), + }) + end) + + it("supports child with child.grow", function() + layout = get_initial_layout({ + position = "bottom", + size = 10, + }) + + layout:update(Layout.Box({ + Layout.Box(s1, { size = 20 }), + Layout.Box({ + Layout.Box(s2, { grow = 1 }), + Layout.Box(s3, { grow = 2 }), + }, { dir = "col", grow = 2 }), + Layout.Box(s4, { grow = 1 }), + }, { dir = "row" })) + + layout:mount() + + assert_size(s1.winid, { + width = 20, + height = 10, + }, 2) + + assert_size(s2.winid, { + width = ((base_size.width - 20) / (2 + 1)) * 2, + height = (10 / (1 + 2)) * 1, + }, 1) + + assert_size(s3.winid, { + width = ((base_size.width - 20) / (2 + 1)) * 2, + height = (10 / (1 + 2)) * 2, + }, 1) + + assert_size(s4.winid, { + width = ((base_size.width - 20) / (2 + 1)) * 1, + height = 10, + }) + end) + + it("can change boxes", function() + layout = Layout( + { position = "bottom", size = 10 }, + Layout.Box({ + Layout.Box(s1, { size = "40%" }), + Layout.Box(s2, { size = "60%" }), + }, { dir = "row" }) + ) + + layout:mount() + + eq(vim.fn.winlayout(), { + "col", + { + { "leaf", winid }, + { + "row", + { + { "leaf", s1.winid }, + { "leaf", s2.winid }, + }, + }, + }, + }) + + layout:update(Layout.Box({ + Layout.Box({ + Layout.Box(s1, { size = "40%" }), + Layout.Box(s2, { size = "60%" }), + }, { dir = "col", size = "60%" }), + Layout.Box(s3, { size = "40%" }), + }, { dir = "row" })) + + eq(vim.fn.winlayout(), { + "col", + { + { "leaf", winid }, + { + "row", + { + { + "col", + { + { "leaf", s1.winid }, + { "leaf", s2.winid }, + }, + }, + { "leaf", s3.winid }, + }, + }, + }, + }) + + layout:update(Layout.Box({ + Layout.Box({ + Layout.Box(s1, { size = "40%" }), + Layout.Box(s2, { size = "60%" }), + }, { dir = "col", size = "60%" }), + Layout.Box(s4, { size = "40%" }), + }, { dir = "row" })) + + eq(vim.fn.winlayout(), { + "col", + { + { "leaf", winid }, + { + "row", + { + { + "col", + { + { "leaf", s1.winid }, + { "leaf", s2.winid }, + }, + }, + { "leaf", s4.winid }, + }, + }, + }, + }) + + eq(s3.winid, nil) + end) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/nui.nvim/tests/nui/layout/utils_spec.lua b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/layout/utils_spec.lua new file mode 100644 index 0000000..7f7851c --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/layout/utils_spec.lua @@ -0,0 +1,144 @@ +pcall(require, "luacov") + +local utils = require("nui.layout.utils") +local h = require("tests.helpers") + +local eq = h.eq + +describe("nui.layout", function() + describe("utils", function() + describe("parse_relative", function() + local fallback_winid = 17 + + it("works for type=buf", function() + local relative = { + type = "buf", + position = { row = 2, col = 4 }, + winid = 42, + } + + local result = utils.parse_relative(relative, fallback_winid) + + eq(result, { + relative = "win", + win = relative.winid, + bufpos = { + relative.position.row, + relative.position.col, + }, + }) + end) + + it("works for type=cursor", function() + local relative = { + type = "cursor", + winid = 42, + } + + local result = utils.parse_relative(relative, fallback_winid) + + eq(result, { + relative = relative.type, + win = relative.winid, + }) + end) + + it("works for type=editor", function() + local relative = { + type = "editor", + winid = 42, + } + + local result = utils.parse_relative(relative, fallback_winid) + + eq(result, { + relative = relative.type, + win = relative.winid, + }) + end) + + it("works for type=win", function() + local relative = { + type = "win", + winid = 42, + } + + local result = utils.parse_relative(relative, fallback_winid) + + eq(result, { + relative = relative.type, + win = relative.winid, + }) + end) + + it("uses fallback_winid if relative.winid is nil", function() + local relative = { + type = "win", + } + + local result = utils.parse_relative(relative, fallback_winid) + + eq(result, { + relative = relative.type, + win = fallback_winid, + }) + end) + end) + + describe("get_container_info", function() + it("works for relative=editor", function() + local result = utils.get_container_info({ + relative = "editor", + }) + + eq(result, { + relative = "editor", + size = { + width = vim.o.columns, + height = vim.o.lines, + }, + type = "editor", + }) + end) + + it("works for relative=cursor", function() + local winid = vim.api.nvim_get_current_win() + + local result = utils.get_container_info({ + relative = "cursor", + win = 0, + }) + + eq(result, { + relative = "cursor", + size = { + width = vim.api.nvim_win_get_width(winid), + height = vim.api.nvim_win_get_height(winid), + }, + type = "window", + winid = winid, + }) + end) + + it("works for relative=win w/ bufpos", function() + local winid = vim.api.nvim_get_current_win() + + local result = utils.get_container_info({ + relative = "win", + win = winid, + bufpos = { 2, 4 }, + }) + + eq(result, { + relative = "buf", + size = { + width = vim.api.nvim_win_get_width(winid), + height = vim.api.nvim_win_get_height(winid), + }, + type = "window", + winid = winid, + }) + end) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/nui.nvim/tests/nui/line/init_spec.lua b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/line/init_spec.lua new file mode 100644 index 0000000..ba7c374 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/line/init_spec.lua @@ -0,0 +1,164 @@ +pcall(require, "luacov") + +local Line = require("nui.line") +local Text = require("nui.text") +local h = require("tests.helpers") + +local eq = h.eq + +describe("nui.line", function() + it("can accept initial nui.text objects", function() + local t1, t2 = Text("One"), Text("Two") + local line = Line({ t1, t2 }) + + eq(#line._texts, 2) + end) + + describe("method :append", function() + it("returns nui.text for string parameter", function() + local line = Line() + local text = line:append("One") + + eq(type(text.content), "function") + end) + + it("returns nui.text for nui.text parameter", function() + local line = Line() + local text = Text("One") + local ret_text = line:append(text) + + eq(text == ret_text, true) + eq(type(ret_text.content), "function") + end) + + it("returns nui.line for nui.line parameter", function() + local line = Line() + + local content_line = Line({ Text("One"), Text("Two") }) + + local ret_content_line = line:append(content_line) + + eq(content_line == ret_content_line, true) + eq(type(ret_content_line.append), "function") + end) + + it("stores and returns block with same reference", function() + local line = Line() + + local text_one = line:append("One") + + eq(line._texts[1] == text_one, true) + + local text_two = Text("Two") + local ret_text_two = line:append(text_two) + + eq(text_two == ret_text_two, true) + eq(line._texts[2] == text_two, true) + eq(line._texts[2] == ret_text_two, true) + + local text_three = Text("Three") + local text_four = Text("Four") + local content_line = Line({ text_three, text_four }) + local ret_content_line = line:append(content_line) + + eq(content_line == ret_content_line, true) + eq(line._texts[3] == content_line._texts[1], true) + eq(line._texts[4] == content_line._texts[2], true) + end) + end) + + describe("method :content", function() + it("returns whole text content", function() + local line = Line() + line:append("One") + line:append("Two") + + eq(line:content(), "OneTwo") + end) + end) + + describe("method :width", function() + it("returns whole text width", function() + local line = Line() + line:append("One") + line:append("Two") + + eq(line:width(), 6) + end) + end) + + describe("method", function() + local winid, bufnr + + before_each(function() + winid = vim.api.nvim_get_current_win() + bufnr = vim.api.nvim_create_buf(false, true) + + vim.api.nvim_win_set_buf(winid, bufnr) + end) + + after_each(function() + vim.api.nvim_buf_delete(bufnr, { force = true }) + end) + + describe(":highlight", function() + local hl_group_one, hl_group_two, ns, ns_id + local linenr + local t1, t2, t3, t4 + local line + + before_each(function() + hl_group_one = "NuiTextTestOne" + hl_group_two = "NuiTextTestTwo" + ns = "NuiTest" + ns_id = vim.api.nvim_create_namespace(ns) + + linenr = 1 + + t1 = Text("One") + t2 = Text("Two", hl_group_one) + t3 = Text("Three", hl_group_two) + t4 = Text("Four") + + line = Line({ t1, t2, t3, t4 }) + end) + + it("is applied with :render", function() + line:render(bufnr, ns_id, linenr) + + h.assert_highlight(bufnr, ns_id, linenr, t2:content(), hl_group_one) + h.assert_highlight(bufnr, ns_id, linenr, t3:content(), hl_group_two) + end) + + it("can highlight existing buffer line", function() + vim.api.nvim_buf_set_lines( + bufnr, + linenr - 1, + -1, + false, + { t1:content() .. t2:content() .. t3:content() .. t4:content() } + ) + + line:highlight(bufnr, ns_id, linenr) + + h.assert_highlight(bufnr, ns_id, linenr, t2:content(), hl_group_one) + h.assert_highlight(bufnr, ns_id, linenr, t3:content(), hl_group_two) + end) + end) + + describe(":render", function() + it("works", function() + local linenr = 1 + + local line = Line() + line:append("4") + line:append("2") + line:render(bufnr, -1, linenr) + + h.assert_buf_lines(bufnr, { + "42", + }) + end) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/nui.nvim/tests/nui/menu/init_spec.lua b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/menu/init_spec.lua new file mode 100644 index 0000000..5112dd2 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/menu/init_spec.lua @@ -0,0 +1,620 @@ +pcall(require, "luacov") + +local Menu = require("nui.menu") +local Layout = require("nui.layout") +local Line = require("nui.line") +local Text = require("nui.text") +local h = require("tests.helpers") +local spy = require("luassert.spy") + +local eq, feedkeys = h.eq, h.feedkeys + +describe("nui.menu", function() + local callbacks + local popup_options + local menu + + before_each(function() + callbacks = { + on_change = function() end, + on_submit = function() end, + } + + popup_options = { + relative = "win", + position = "50%", + } + end) + + after_each(function() + if menu then + menu:unmount() + menu = nil + end + end) + + describe("method :new", function() + it("works with menu", function() + menu = Menu:new(popup_options, { + lines = { + Menu.item("a"), + }, + }) + + menu:mount() + + h.assert_buf_lines(menu.bufnr, { + "a", + }) + end) + end) + + describe("o.keymap", function() + it("supports multiple keys as table", function() + local on_change = spy.on(callbacks, "on_change") + + local lines = { + Menu.item("Item 1", { id = 1 }), + Menu.item("Item 2", { id = 2 }), + Menu.item("Item 3", { id = 3 }), + } + + menu = Menu(popup_options, { + keymap = { + focus_next = { "j", "s" }, + focus_prev = { "k", "w" }, + }, + lines = lines, + on_change = on_change, + }) + + menu:mount() + + feedkeys("j", "x") + assert.spy(on_change).called_with(lines[2], menu) + on_change:clear() + + feedkeys("s", "x") + assert.spy(on_change).called_with(lines[3], menu) + on_change:clear() + + feedkeys("w", "x") + assert.spy(on_change).called_with(lines[2], menu) + on_change:clear() + + feedkeys("k", "x") + assert.spy(on_change).called_with(lines[1], menu) + on_change:clear() + end) + + it("supports single key as string", function() + local on_change = spy.on(callbacks, "on_change") + + local lines = { + Menu.item("Item 1", { id = 1 }), + Menu.item("Item 2", { id = 2 }), + Menu.item("Item 3", { id = 3 }), + } + + menu = Menu(popup_options, { + keymap = { + focus_next = "s", + focus_prev = "w", + }, + lines = lines, + on_change = on_change, + }) + + menu:mount() + + feedkeys("s", "x") + assert.spy(on_change).called_with(lines[2], menu) + on_change:clear() + + feedkeys("w", "x") + assert.spy(on_change).called_with(lines[1], menu) + on_change:clear() + end) + end) + + describe("size", function() + it("respects o.min_width", function() + local min_width = 3 + + local items = { + Menu.item("A"), + Menu.separator("*"), + Menu.item("B"), + } + + menu = Menu(popup_options, { + lines = items, + min_width = min_width, + }) + + menu:mount() + + eq(vim.api.nvim_win_get_width(menu.winid), min_width) + + h.assert_buf_lines(menu.bufnr, { + "A", + " * ", + "B", + }) + end) + + it("respects o.max_width", function() + local max_width = 6 + + local items = { + Menu.item("Item 1"), + Menu.separator("*"), + Menu.item("Item Number Two"), + } + + menu = Menu(popup_options, { + lines = items, + max_width = max_width, + }) + + menu:mount() + + eq(vim.api.nvim_win_get_width(menu.winid), max_width) + + h.assert_buf_lines(menu.bufnr, { + "Item 1", + " * ", + "Item …", + }) + end) + + it("respects o.min_height", function() + local min_height = 3 + + local items = { + Menu.item("A"), + Menu.separator("*"), + Menu.item("B"), + } + + menu = Menu(popup_options, { + lines = items, + min_height = min_height, + }) + + menu:mount() + + eq(vim.api.nvim_win_get_height(menu.winid), min_height) + end) + + it("respects o.max_height", function() + local max_height = 2 + + local items = { + Menu.item("A"), + Menu.separator("*"), + Menu.item("B"), + } + + menu = Menu(popup_options, { + lines = items, + max_height = max_height, + }) + + menu:mount() + + eq(vim.api.nvim_win_get_height(menu.winid), max_height) + end) + end) + + it("calls o.on_change item focus is changed", function() + local on_change = spy.on(callbacks, "on_change") + + local lines = { + Menu.item("Item 1", { id = 1 }), + Menu.item("Item 2", { id = 2 }), + } + + menu = Menu(popup_options, { + lines = lines, + on_change = on_change, + }) + + menu:mount() + + -- initial focus + assert.spy(on_change).called_with(lines[1], menu) + on_change:clear() + + feedkeys("j", "x") + assert.spy(on_change).called_with(lines[2], menu) + on_change:clear() + + feedkeys("j", "x") + assert.spy(on_change).called_with(lines[1], menu) + on_change:clear() + + feedkeys("k", "x") + assert.spy(on_change).called_with(lines[2], menu) + on_change:clear() + end) + + it("calls o.on_submit when item is submitted", function() + local on_submit = spy.on(callbacks, "on_submit") + + local lines = { + Menu.item("Item 1", { id = 1 }), + Menu.item("Item 2", { id = 2 }), + } + + menu = Menu(popup_options, { + lines = lines, + on_submit = on_submit, + }) + + menu:mount() + + feedkeys("j", "x") + feedkeys("", "x") + + assert.spy(on_submit).called_with(lines[2]) + end) + + it("calls o.on_close when menu is closed", function() + local on_close = spy.on(callbacks, "on_close") + + local lines = { + Menu.item("Item 1", { id = 1 }), + Menu.item("Item 2", { id = 2 }), + } + + menu = Menu(popup_options, { + lines = lines, + on_close = on_close, + }) + + menu:mount() + + feedkeys("", "x") + + assert.spy(on_close).called_with() + end) + + describe("item", function() + it("is prepared using o.prepare_item if provided", function() + local items = { + Menu.item("A"), + Menu.separator("*"), + Menu.item("B"), + } + + local function prepare_item(item) + return "-" .. item.text .. "-" + end + + menu = Menu(popup_options, { + lines = items, + prepare_item = prepare_item, + }) + + menu:mount() + + h.assert_buf_lines(menu.bufnr, vim.tbl_map(prepare_item, items)) + end) + + it("is prepared when o.prepare_item is not provided", function() + local items = { + Menu.item("A"), + Menu.separator("*"), + Menu.item("B"), + } + + popup_options.border = "single" + + menu = Menu(popup_options, { + lines = items, + }) + + menu:mount() + + h.assert_buf_lines(menu.bufnr, { + "A", + "─*──", + "B", + }) + end) + + it("is skipped respecting o.should_skip_item if provided", function() + local on_change = spy.on(callbacks, "on_change") + + local items = { + Menu.item("-"), + Menu.item("A", { id = 1 }), + Menu.item("-"), + Menu.item("B", { id = 2 }), + Menu.item("-"), + } + + menu = Menu(popup_options, { + lines = items, + on_change = on_change, + should_skip_item = function(item) + return not item.id + end, + }) + + menu:mount() + + assert.spy(on_change).called_with(items[2], menu) + on_change:clear() + + feedkeys("j", "x") + assert.spy(on_change).called_with(items[4], menu) + on_change:clear() + + feedkeys("j", "x") + assert.spy(on_change).called_with(items[2], menu) + on_change:clear() + end) + + it("supports table with key .text", function() + local text = "text" + + local items = { + Menu.item({ text = text }), + } + + menu = Menu(popup_options, { + lines = items, + }) + + menu:mount() + + h.assert_buf_lines(menu.bufnr, { + text, + }) + end) + + it("supports nui.text", function() + local hl_group = "NuiMenuTest" + local text = "text" + local items = { + Menu.item(Text(text, hl_group)), + } + + menu = Menu(popup_options, { + lines = items, + }) + + menu:mount() + + h.assert_buf_lines(menu.bufnr, { + text, + }) + + h.assert_highlight(menu.bufnr, menu.ns_id, 1, text, hl_group) + end) + + it("supports nui.line", function() + local hl_group = "NuiMenuTest" + local text = "text" + local items = { + Menu.item(Line({ Text(text, hl_group) })), + } + + menu = Menu(popup_options, { + lines = items, + }) + + menu:mount() + + h.assert_buf_lines(menu.bufnr, { + text, + }) + + h.assert_highlight(menu.bufnr, menu.ns_id, 1, text, hl_group) + end) + + it("content longer than max_width is truncated", function() + local items = { + Menu.item({ text = "Item 10 -" }), + Menu.item(Text("Item 20 -")), + Menu.item(Line({ Text("Item 30 -") })), + Menu.item(Line({ Text("Item 40"), Text(" -") })), + Menu.item(Line({ Text("Item 50 -"), Text(" -") })), + } + + menu = Menu(popup_options, { + max_width = 7, + lines = items, + }) + + menu:mount() + + h.assert_buf_lines(menu.bufnr, { + "Item 1…", + "Item 2…", + "Item 3…", + "Item 4…", + "Item 5…", + }) + end) + end) + + it("can truncate content longer than max_width w/ multi-byte chars", function() + menu = Menu(popup_options, { + lines = { + Menu.item("中文长度测试"), + Menu.item("Test中英文测试"), + Menu.item("Long Long Group"), + }, + max_width = 11, + }) + + menu:mount() + + h.assert_buf_lines(menu.bufnr, { + "中文长度测…", + "Test中英文…", + "Long Long …", + }) + end) + + describe("separator", function() + it("text supports string", function() + menu = Menu(popup_options, { + lines = { + Menu.item("A"), + Menu.separator("Group"), + }, + min_width = 10, + }) + + menu:mount() + + h.assert_buf_lines(menu.bufnr, { + "A", + " Group ", + }) + end) + + it("content longer than max_width is truncated", function() + menu = Menu(popup_options, { + lines = { + Menu.item("A"), + Menu.separator("Long Long Group"), + }, + max_width = 10, + }) + + menu:mount() + + h.assert_buf_lines(menu.bufnr, { + "A", + " Long Lo… ", + }) + end) + + it("text supports nui.text", function() + local hl_group = "NuiMenuTest" + local text = "Group" + + menu = Menu(popup_options, { + lines = { + Menu.item("A"), + Menu.separator(Text(text, hl_group)), + }, + min_width = 10, + }) + + menu:mount() + + h.assert_buf_lines(menu.bufnr, { + "A", + " Group ", + }) + + h.assert_highlight(menu.bufnr, menu.ns_id, 2, text, hl_group) + end) + + it("text supports nui.line", function() + local hl_group = "NuiMenuTest" + local text = "Group" + + menu = Menu(popup_options, { + lines = { + Menu.item("A"), + Menu.separator(Line({ Text(text, hl_group), Text(" nui.text") })), + }, + min_width = 10, + }) + + menu:mount() + + h.assert_buf_lines(menu.bufnr, { + "A", + " Group nui.t… ", + }) + + h.assert_highlight(menu.bufnr, menu.ns_id, 2, text, hl_group) + end) + + it("o.char supports string", function() + menu = Menu(popup_options, { + lines = { + Menu.item("A"), + Menu.separator("Group", { + char = "*", + text_align = "right", + }), + }, + min_width = 10, + }) + + menu:mount() + + h.assert_buf_lines(menu.bufnr, { + "A", + "****Group*", + }) + end) + + it("o.char supports nui.text", function() + local hl_group = "NuiMenuTest" + + menu = Menu(popup_options, { + lines = { + Menu.item("A"), + Menu.separator("Group", { + char = Text("*", hl_group), + text_align = "center", + }), + }, + min_width = 10, + }) + + menu:mount() + + h.assert_buf_lines(menu.bufnr, { + "A", + "**Group***", + }) + + local linenr = 2 + + local extmarks = h.get_line_extmarks(menu.bufnr, menu.ns_id, linenr) + + eq(#extmarks, 4) + h.assert_extmark(extmarks[1], linenr, "*", hl_group) + h.assert_extmark(extmarks[2], linenr, "*", hl_group) + h.assert_extmark(extmarks[3], linenr, "**", hl_group) + h.assert_extmark(extmarks[4], linenr, "*", hl_group) + end) + end) + + describe("w/ Layout", function() + it("can be used", function() + menu = Menu({}, { + lines = { + Menu.item("A"), + }, + }) + + local layout = Layout( + { + position = "50%", + size = "100%", + }, + Layout.Box({ + Layout.Box(menu, { size = "100%" }), + }) + ) + + layout:mount() + + h.assert_buf_lines(menu.bufnr, { + "A", + }) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/nui.nvim/tests/nui/object/init_spec.lua b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/object/init_spec.lua new file mode 100644 index 0000000..9abc914 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/object/init_spec.lua @@ -0,0 +1,413 @@ +pcall(require, "luacov") + +local h = require("tests.helpers") +local Object = require("nui.object") +local spy = require("luassert.spy") + +local function assert_class(Class, SuperClass, name) + h.eq(type(Class), "table") + + h.eq(Class.super, SuperClass) + h.eq(Class.name, name) + h.eq(tostring(Class), "class " .. name) + + h.eq(type(Class.new), "function") + h.eq(type(Class.extend), "function") + + local is_callable = pcall(function() + return Class() + end) + h.eq(is_callable, true) +end + +local function assert_instance(instance, Class) + h.eq(instance.class, Class) + h.eq(tostring(instance), "instance of class " .. Class.name) + + h.eq(instance.name, nil) + h.eq(instance.super, nil) + h.eq(instance.static, nil) + + h.eq(instance.new, nil) + h.eq(instance.extend, nil) +end + +local function create_classes(...) + local by_name = {} + local classes = {} + + for i, def in ipairs({ ... }) do + if type(def) == "string" then + local class = Object(def) + assert_class(class, nil, def) + by_name[def] = class + classes[i] = class + elseif type(def) == "table" then + local super = type(def[2]) == "table" and def[2] or (by_name[def[2]] and by_name[def[2]] or nil) + local class = super and super:extend(def[1]) or Object(def[1]) + assert_class(class, super, def[1]) + by_name[def[1]] = class + classes[i] = class + else + error("invalid argument") + end + end + + return unpack(classes) +end + +describe("nui.object", function() + describe("class", function() + it("can be created", function() + local Class = Object("Class") + assert_class(Class, nil, "Class") + end) + + describe("static", function() + describe("method", function() + describe(":new", function() + it("is called when creating instance", function() + local Class = Object("Class") + + spy.on(Class.static, "new") + Class() + assert.spy(Class.static.new).called_with(Class) + Class.static.new:revert() + + spy.on(Class.static, "new") + Class:new() + assert.spy(Class.static.new).called_with(Class) + Class.static.new:revert() + end) + + it("creates new instance", function() + local Class = Object("Class") + + local instance = Class:new() + assert_instance(instance, Class) + end) + end) + + describe(":extend", function() + it("creates subclass", function() + local Class = Object("Class") + + local SubClass = Class:extend("SubClass") + assert_class(SubClass, Class, "SubClass") + end) + end) + + describe(":is_subclass_of", function() + it("works", function() + local A, B, C = create_classes("A", { "B", "A" }, { "C", "B" }) + + for _, class in ipairs({ A, B, C }) do + h.eq(class.is_subclass_of, Object.is_subclass) + end + + h.eq(A:is_subclass_of(A), false) + h.eq(A:is_subclass_of(B), false) + h.eq(A:is_subclass_of(C), false) + + h.eq(B:is_subclass_of(A), true) + h.eq(B:is_subclass_of(B), false) + h.eq(B:is_subclass_of(C), false) + + h.eq(C:is_subclass_of(A), true) + h.eq(C:is_subclass_of(B), true) + h.eq(C:is_subclass_of(C), false) + end) + end) + end) + + local function define_static_say_level(A) + A.static.level = 1 + function A.static.say_level(class) + return "Level: " .. class.level + end + + h.eq(A.level, 1) + h.eq(A:say_level(), "Level: 1") + end + + it("can be defined for class", function() + local A = create_classes("A") + define_static_say_level(A) + end) + + it("is inherited by subclass", function() + local A, B = create_classes("A", { "B", "A" }) + + define_static_say_level(A) + + h.eq(B.level, 1) + h.eq(B:say_level(), "Level: 1") + + local C, D = create_classes({ "C", A }, { "D", B }) + + h.eq(C.level, 1) + h.eq(C:say_level(), "Level: 1") + + h.eq(D.level, 1) + h.eq(D:say_level(), "Level: 1") + end) + + it("can be redefined for subclass", function() + local A = create_classes("A") + define_static_say_level(A) + + local B = create_classes({ "B", A }) + + B.static.level = 2 + h.eq(B:say_level(), "Level: 2") + + function B.static.say_level(class) + return "LEVEL: " .. class.level + end + h.eq(B:say_level(), "LEVEL: 2") + + local C, D = create_classes({ "C", A }, { "D", B }) + + C.static.level = 2 + h.eq(C:say_level(), "Level: 2") + + D.static.level = 3 + h.eq(D:say_level(), "LEVEL: 3") + end) + + it("for subclass does not affect super", function() + local A = create_classes("A") + define_static_say_level(A) + + local B = create_classes({ "B", A }) + + B.static.level = 2 + function B.static.say_level(class) + return "LEVEL: " .. class.level + end + + h.eq(A:say_level(), "Level: 1") + + local C = create_classes({ "C", B }) + + function C.static.say_name(class) + return class.name + end + + h.eq(C:say_name(), "C") + + h.eq(type(C.say_name), "function") + h.eq(type(B.say_name), "nil") + h.eq(type(A.say_name), "nil") + end) + end) + + describe("instance", function() + it("can be created", function() + local A = create_classes("A") + + local a = A:new() + assert_instance(a, A) + end) + + describe("method", function() + describe(":is_instance_of", function() + it("works", function() + local A, B, C, D = create_classes("A", { "B", "A" }, { "C", "B" }, "D") + + local a, b, c, d = A:new(), B:new(), C:new(), D:new() + + for _, instance in ipairs({ a, b, c, d }) do + h.eq(instance.is_instance_of, Object.is_instance) + end + + h.eq(a:is_instance_of(A), true) + h.eq(a:is_instance_of(B), false) + h.eq(a:is_instance_of(C), false) + h.eq(a:is_instance_of(D), false) + + h.eq(b:is_instance_of(A), true) + h.eq(b:is_instance_of(B), true) + h.eq(b:is_instance_of(C), false) + h.eq(b:is_instance_of(D), false) + + h.eq(c:is_instance_of(A), true) + h.eq(c:is_instance_of(B), true) + h.eq(c:is_instance_of(C), true) + h.eq(c:is_instance_of(D), false) + + h.eq(d:is_instance_of(A), false) + h.eq(d:is_instance_of(B), false) + h.eq(d:is_instance_of(C), false) + h.eq(d:is_instance_of(D), true) + end) + end) + + it("can be defined", function() + local A = create_classes("A") + + function A:before_instance_creation() + return "before " .. self.class.name .. " instance" + end + + local a = A:new() + + function A:after_instance_creation() + return "after " .. self.class.name .. " instance" + end + + h.eq(a:before_instance_creation(), "before A instance") + h.eq(a:after_instance_creation(), "after A instance") + end) + + it("can be inherited", function() + local A, B = create_classes("A", { "B", "A" }) + + function A:say_class_name() + return self.class.name + end + + local a = A:new() + h.eq(a:say_class_name(), "A") + + local b = B:new() + h.eq(b:say_class_name(), "B") + + local C = create_classes({ "C", B }) + + local c = C:new() + h.eq(c:say_class_name(), "C") + end) + + it("can be redefined", function() + local A, B = create_classes("A", { "B", "A" }) + + function A:say_class_name() + return self.class.name + end + + local a = A:new() + h.eq(a:say_class_name(), "A") + + function B:say_class_name() + return string.lower(self.class.name) + end + + local b = B:new() + h.eq(b:say_class_name(), "b") + + local C = create_classes({ "C", B }) + + local c = C:new() + h.eq(c:say_class_name(), "c") + + function C:say_class_name() + return string.rep(self.class.name, 3) + end + + h.eq(c:say_class_name(), "CCC") + + C.say_class_name = nil + + h.eq(c:say_class_name(), "c") + + B.say_class_name = nil + + h.eq(c:say_class_name(), "C") + end) + end) + + describe("metamethod", function() + describe("__index", function() + it("can be set to table", function() + local A = create_classes("A") + + function A:upper(str) -- luacheck: no unused args + return string.upper(str) + end + + A.__index = { + upper = function(_, str) + return str + end, + lower = function(_, str) + return string.lower(str) + end, + } + + local a = A() + + h.eq(a:upper("y"), "Y") + + h.eq(a:lower("Y"), "y") + + A.__index = nil + + h.eq(type(a.lower), "nil") + end) + + it("can be set to function", function() + local A = create_classes("A") + + function A:upper(str) -- luacheck: no unused args + return string.upper(str) + end + + local index = { + upper = function(self, str) -- luacheck: no unused args + return str + end, + lower = function(self, str) -- luacheck: no unused args + return string.lower(str) + end, + } + + A.__index = function(self, key) -- luacheck: no unused args + return index[key] + end + + local a = A() + + h.eq(a:upper("y"), "Y") + + h.eq(a:lower("Y"), "y") + + A.__index = nil + + h.eq(type(a.lower), "nil") + end) + end) + + describe("__tostring", function() + it("can be redefined", function() + local A, B = create_classes("A", { "B", "A" }) + + local a = A() + + h.eq(tostring(a), "instance of class A") + + function A:__tostring() + return "class " .. self.class.name .. "'s child" + end + + h.eq(tostring(a), "class A's child") + + local b = B() + + h.eq(tostring(b), "class B's child") + + function B:__tostring() + return "child of " .. self.class.name + end + + h.eq(tostring(b), "child of B") + + B.__tostring = nil + + h.eq(tostring(b), "class B's child") + end) + end) + end) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/nui.nvim/tests/nui/popup/border_spec.lua b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/popup/border_spec.lua new file mode 100644 index 0000000..73a65be --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/popup/border_spec.lua @@ -0,0 +1,1031 @@ +pcall(require, "luacov") + +local Layout = require("nui.layout") +local Popup = require("nui.popup") +local Line = require("nui.line") +local Text = require("nui.text") +local h = require("tests.helpers") + +local eq = h.eq + +describe("nui.popup", function() + local popup_options = {} + local popup + + before_each(function() + popup_options = { + ns_id = vim.api.nvim_create_namespace("NuiTest"), + position = "50%", + size = { + height = 2, + width = 8, + }, + } + end) + + after_each(function() + if popup then + popup:unmount() + popup = nil + end + end) + + describe("(#deprecated) border.highlight", function() + it("works for 'hl_group'", function() + local hl_group = "NuiPopupTest" + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "rounded", + highlight = hl_group, + padding = { 0 }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + eq(vim.api.nvim_win_get_option(popup.border.winid, "winhighlight"), "FloatBorder:" .. hl_group) + end) + + it("works for 'FloatBorder:hl_group'", function() + local hl_group = "NuiPopupTest" + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "rounded", + highlight = "FloatBorder:" .. hl_group, + padding = { 0 }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + eq(vim.api.nvim_win_get_option(popup.border.winid, "winhighlight"), "FloatBorder:" .. hl_group) + end) + end) + + describe("border.padding", function() + local function assert_padding(padding, target_popup) + local border_char_size = 1 + + local popup_win_config = vim.api.nvim_win_get_config(target_popup.winid) + eq(popup_win_config.win, target_popup.border.winid) + eq(popup_win_config.row, border_char_size + padding.top) + eq(popup_win_config.col, border_char_size + padding.left) + + local border_win_config = vim.api.nvim_win_get_config(target_popup.border.winid) + eq(border_win_config.width, popup_options.size.width + border_char_size * 2 + padding.right + padding.left) + eq(border_win_config.height, popup_options.size.height + border_char_size * 2 + padding.top + padding.bottom) + end + + it("supports list table", function() + local padding = { + top = 2, + right = 2, + bottom = 1, + left = 1, + } + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "rounded", + padding = { padding.top, padding.right, padding.bottom, padding.left }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + assert_padding(padding, popup) + end) + + it("supports partial list table", function() + local padding = { + top = 2, + right = 1, + bottom = 2, + left = 1, + } + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "rounded", + padding = { padding.top, padding.right }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + assert_padding(padding, popup) + end) + + it("supports map table", function() + local padding = { + top = 2, + right = 2, + bottom = 1, + left = 1, + } + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "rounded", + padding = padding, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + assert_padding(padding, popup) + end) + end) + + describe("border.style", function() + describe("for complex border", function() + it("is normalized", function() + local index_name = { + "top_left", + "top", + "top_right", + "right", + "bottom_right", + "bottom", + "bottom_left", + "left", + } + + local char_map + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + padding = { 0 }, + }, + }) + + popup = Popup(popup_options) + char_map = popup.border._.char + eq(char_map, "none") + + popup_options.border.style = "shadow" + popup = Popup(popup_options) + char_map = popup.border._.char + eq(char_map, "shadow") + + popup_options.border.style = "rounded" + popup = Popup(popup_options) + char_map = popup.border._.char + for _, name in ipairs(index_name) do + eq(type(char_map[name]:content()), "string") + eq(char_map[name].extmark.hl_group, "FloatBorder") + end + + popup_options.border.style = h.popup.create_border_style_list() + popup_options.border.style[1] = { popup_options.border.style[1], "TopLeft" } + popup_options.border.style[2] = Text(popup_options.border.style[2]) + popup_options.border.style[3] = Text(popup_options.border.style[3], "TopRight") + popup_options.border.style[6] = { popup_options.border.style[6] } + popup = Popup(popup_options) + char_map = popup.border._.char + for _, name in ipairs(index_name) do + eq(type(char_map[name]:content()), "string") + if name == "top_left" then + eq(char_map[name].extmark.hl_group, "TopLeft") + elseif name == "top_right" then + eq(char_map[name].extmark.hl_group, "TopRight") + else + eq(char_map[name].extmark.hl_group, "FloatBorder") + end + end + + popup_options.border.style = h.popup.create_border_style_map() + popup_options.border.style.top_left = { popup_options.border.style.top_left, "TopLeft" } + popup_options.border.style.top = Text(popup_options.border.style.top) + popup_options.border.style.top_right = Text(popup_options.border.style.top_right, "TopRight") + popup_options.border.style.bottom = { popup_options.border.style.bottom } + popup = Popup(popup_options) + char_map = popup.border._.char + for _, name in ipairs(index_name) do + eq(type(char_map[name]:content()), "string") + if name == "top_left" then + eq(char_map[name].extmark.hl_group, "TopLeft") + elseif name == "top_right" then + eq(char_map[name].extmark.hl_group, "TopRight") + else + eq(char_map[name].extmark.hl_group, "FloatBorder") + end + end + end) + + it("supports string name", function() + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "rounded", + padding = { 0 }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + h.assert_buf_lines(popup.border.bufnr, { + "╭────────╮", + "│ │", + "│ │", + "╰────────╯", + }) + end) + + it("supports list table", function() + local style = h.popup.create_border_style_list() + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = style, + padding = { 0 }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + h.popup.assert_border_lines(popup_options, popup.border.bufnr) + end) + + it("supports partial list table", function() + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = { "-" }, + padding = { 0 }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + popup_options.border.style = { "-", "-", "-", "-", "-", "-", "-", "-" } + + h.popup.assert_border_lines(popup_options, popup.border.bufnr) + end) + + it("supports map table", function() + local style = h.popup.create_border_style_map() + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = style, + padding = { 0 }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + h.popup.assert_border_lines(popup_options, popup.border.bufnr) + end) + + it("supports (char, hl_group) tuple in partial list table", function() + local hl_group = "NuiPopupTest" + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = { { "-", hl_group } }, + padding = { 0 }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + popup_options.border.style = { "-", "-", "-", "-", "-", "-", "-", "-" } + + h.popup.assert_border_lines(popup_options, popup.border.bufnr) + h.popup.assert_border_highlight(popup_options, popup.border.bufnr, hl_group, true) + end) + + it("supports (char, hl_group) tuple in map table", function() + local hl_group = "NuiPopupTest" + local style = h.popup.create_border_style_map_with_tuple(hl_group) + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = style, + padding = { 0 }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + h.popup.assert_border_lines(popup_options, popup.border.bufnr) + h.popup.assert_border_highlight(popup_options, popup.border.bufnr, hl_group) + end) + + it("supports nui.text in map table", function() + local hl_group = "NuiPopupTest" + local style = h.popup.create_border_style_map_with_nui_text(hl_group) + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = style, + padding = { 0 }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + h.popup.assert_border_lines(popup_options, popup.border.bufnr) + h.popup.assert_border_highlight(popup_options, popup.border.bufnr, hl_group) + end) + end) + + describe("for simple border", function() + it("supports nui.text as char", function() + local hl_group = "NuiPopupTest" + + local style = h.popup.create_border_style_list() + style[2] = Text(style[2], hl_group) + style[6] = Text(style[6]) + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = style, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + local win_config = vim.api.nvim_win_get_config(popup.winid) + + eq(win_config.border[2], { style[2]:content(), hl_group }) + eq(win_config.border[6], { style[6]:content(), "FloatBorder" }) + end) + + it("supports (char, hl_group) tuple as char", function() + local hl_group = "NuiPopupTest" + + local style = h.popup.create_border_style_list() + style[2] = { style[2], hl_group } + style[6] = { style[6] } + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = style, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + local win_config = vim.api.nvim_win_get_config(popup.winid) + + eq(win_config.border[2], { style[2][1], style[2][2] }) + eq(win_config.border[6], { style[6][1], "FloatBorder" }) + end) + + it("throws if map table missing keys", function() + local style = h.popup.create_border_style_map() + style["top"] = nil + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = style, + }, + }) + + local ok, err = pcall(Popup, popup_options) + eq(ok, false) + eq(type(string.match(err, "missing named border: top")), "string") + end) + end) + end) + + describe("border.text", function() + it("throws error if borderless", function() + local text = "popup" + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "none", + text = { + top = text, + }, + }, + }) + + local ok, err = pcall(Popup, popup_options) + eq(ok, false) + eq(type(string.match(err, "text not supported for style:")), "string") + end) + + it("throws error if invalid border style", function() + local text = "popup" + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "__invalid__", + text = { + top = text, + }, + }, + }) + + local ok, err = pcall(Popup, popup_options) + eq(ok, false) + eq(type(string.match(err, "invalid border style name")), "string") + end) + + it("throws error if invalid align", function() + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "single", + text = { + top = "popup", + top_align = "diagonal", + }, + }, + }) + + local ok, err = pcall(Popup, popup_options) + eq(ok, false) + eq(type(string.match(err, "invalid value align=diagonal")), "string") + end) + + it("supports simple text", function() + local text = "popup" + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "single", + text = { + top = text, + }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + h.assert_buf_lines(popup.border.bufnr, { + "┌─popup──┐", + }, 1, 1) + h.assert_highlight(popup.border.bufnr, popup_options.ns_id, 1, text, "FloatTitle") + end) + + it("supports string[]", function() + local text = { "a", "-", "b" } + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "single", + text = { + top = text, + }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + h.assert_buf_lines(popup.border.bufnr, { + "┌──a-b───┐", + }, 1, 1) + h.assert_highlight(popup.border.bufnr, popup_options.ns_id, 1, "a", "FloatTitle") + h.assert_highlight(popup.border.bufnr, popup_options.ns_id, 1, "-", "FloatTitle") + h.assert_highlight(popup.border.bufnr, popup_options.ns_id, 1, "b", "FloatTitle") + end) + + it("supports nui.text", function() + local text = "popup" + local hl_group = "NuiPopupTest" + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "single", + text = { + top = Text(text), + bottom = Text(text, hl_group), + }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + h.assert_buf_lines(popup.border.bufnr, { + "┌─popup──┐", + }, 1, 1) + h.assert_highlight(popup.border.bufnr, popup_options.ns_id, 1, text, "FloatTitle") + + h.assert_buf_lines(popup.border.bufnr, { + "└─popup──┘", + }, 4, 4) + h.assert_highlight(popup.border.bufnr, popup_options.ns_id, 4, text, hl_group) + end) + + it("supports nui.line", function() + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "single", + text = { + top = Line({ Text("a", "NuiTestA"), Text("-"), Text("b", "NuiTestB") }), + }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + h.assert_buf_lines(popup.border.bufnr, { + "┌──a-b───┐", + }, 1, 1) + h.assert_highlight(popup.border.bufnr, popup_options.ns_id, 1, "a", "NuiTestA") + h.assert_highlight(popup.border.bufnr, popup_options.ns_id, 1, "-", "FloatTitle") + h.assert_highlight(popup.border.bufnr, popup_options.ns_id, 1, "b", "NuiTestB") + end) + + it("supports (text, hl_group)[]", function() + local text = { { "a", "NuiTestA" }, "-", { "b", "NuiTestB" } } + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "single", + text = { + top = text, + }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + h.assert_buf_lines(popup.border.bufnr, { + "┌──a-b───┐", + }, 1, 1) + h.assert_highlight(popup.border.bufnr, popup_options.ns_id, 1, "a", "NuiTestA") + h.assert_highlight(popup.border.bufnr, popup_options.ns_id, 1, "-", "FloatTitle") + h.assert_highlight(popup.border.bufnr, popup_options.ns_id, 1, "b", "NuiTestB") + end) + end) + + describe("method :mount", function() + it("ensure .bufnr is valid", function() + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "single", + text = {}, + }, + }) + + popup = Popup(popup_options) + + popup.border.bufnr = nil + popup:mount() + eq(vim.api.nvim_buf_is_valid(popup.border.bufnr), true) + popup:unmount() + + popup = Popup(popup_options) + vim.api.nvim_buf_delete(popup.border.bufnr, { force = true }) + popup:mount() + eq(vim.api.nvim_buf_is_valid(popup.border.bufnr), true) + popup:unmount() + end) + + it("sets winblend from popup", function() + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "rounded", + text = { + top = "text", + }, + }, + win_options = { + winblend = 20, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + eq(vim.api.nvim_win_get_option(popup.border.winid, "winblend"), 20) + end) + + it("sets winhighlight correctly", function() + local hl_group = "NuiPopupTest" + local winhighlight = "Normal:Normal,FloatBorder:" .. hl_group + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "rounded", + text = { + top = "text", + }, + }, + win_options = { + winhighlight = winhighlight, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + eq(vim.api.nvim_win_get_option(popup.border.winid, "winhighlight"), winhighlight) + end) + + it("does nothing if popup mounted", function() + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "rounded", + text = { + top = "text", + }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + local bufnr, winid = popup.border.bufnr, popup.border.winid + eq(type(bufnr), "number") + eq(type(winid), "number") + + popup.border:mount() + + eq(bufnr, popup.border.bufnr) + eq(winid, popup.border.winid) + end) + end) + + describe("method :unmount", function() + it("does nothing if popup not mounted", function() + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "rounded", + text = { + top = "text", + }, + }, + }) + + popup = Popup(popup_options) + + local bufnr = popup.border.bufnr + eq(type(popup.border.winid), "nil") + + popup.border:unmount() + + eq(popup.border.bufnr, bufnr) + eq(type(popup.border.winid), "nil") + end) + end) + + describe("method :set_style", function() + it("throws when trying to convert complex border to simple", function() + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "rounded", + padding = { 0 }, + }, + }) + + popup = Popup(popup_options) + + local ok, err = pcall(function() + popup.border:set_style("none") + end) + eq(ok, false) + eq(type(string.match(err, "cannot change from previous style to none")), "string") + end) + end) + + describe("method :set_text", function() + it("works before :mount", function() + local text_top, text_bottom = "top", "bot" + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "rounded", + text = { + top = text_top, + top_align = "left", + }, + }, + }) + + popup = Popup(popup_options) + + h.assert_buf_lines(popup.border.bufnr, { + "╭top─────╮", + "│ │", + "│ │", + "╰────────╯", + }) + + popup.border:set_text("top", text_top, "center") + + h.assert_buf_lines(popup.border.bufnr, { + "╭──top───╮", + "│ │", + "│ │", + "╰────────╯", + }) + + popup.border:set_text("top", text_top, "right") + + h.assert_buf_lines(popup.border.bufnr, { + "╭─────top╮", + "│ │", + "│ │", + "╰────────╯", + }) + + local hl_group = "NuiPopupTest" + + popup.border:set_text("bottom", Text(text_bottom, hl_group)) + + popup:mount() + + h.assert_buf_lines(popup.border.bufnr, { + "╭─────top╮", + "│ │", + "│ │", + "╰──bot───╯", + }) + + h.assert_highlight(popup.border.bufnr, popup_options.ns_id, 4, text_bottom, hl_group) + end) + + it("works after :mount", function() + local text_top, text_bottom = "top", "bot" + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "rounded", + text = { + top = text_top, + top_align = "left", + }, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + h.assert_buf_lines(popup.border.bufnr, { + "╭top─────╮", + "│ │", + "│ │", + "╰────────╯", + }) + + popup.border:set_text("top", text_top, "center") + + h.assert_buf_lines(popup.border.bufnr, { + "╭──top───╮", + "│ │", + "│ │", + "╰────────╯", + }) + + popup.border:set_text("top", text_top, "right") + + h.assert_buf_lines(popup.border.bufnr, { + "╭─────top╮", + "│ │", + "│ │", + "╰────────╯", + }) + + local hl_group = "NuiPopupTest" + + popup.border:set_text("bottom", Text(text_bottom, hl_group)) + + h.assert_buf_lines(popup.border.bufnr, { + "╭─────top╮", + "│ │", + "│ │", + "╰──bot───╯", + }) + + h.assert_highlight(popup.border.bufnr, popup_options.ns_id, 4, text_bottom, hl_group) + end) + + it("works w/ Layout before :mount", function() + local text_top = "top" + + popup = Popup({ + border = { + style = "rounded", + text = { + top = text_top, + top_align = "left", + }, + }, + }) + + popup.border:set_text("top", "TOP", "right") + + local layout = Layout( + { + position = popup_options.position, + size = { + width = popup_options.size.width + 2, + height = popup_options.size.height + 2, + }, + }, + Layout.Box({ + Layout.Box(popup, { size = "100%" }), + }) + ) + + layout:mount() + + h.assert_buf_lines(popup.border.bufnr, { + "╭─────TOP╮", + "│ │", + "│ │", + "╰────────╯", + }) + + layout:unmount() + end) + + it("does nothing for simple border", function() + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = "rounded", + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + eq(type(popup.border.bufnr), "nil") + + popup.border:set_text("top", "text") + + eq(type(popup.border.bufnr), "nil") + end) + end) + + describe("method :set_highlight", function() + it("works for simple border", function() + local style = h.popup.create_border_style_map() + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = style, + }, + win_options = { + winhighlight = "Normal:Normal,FloatBorder:Normal", + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + eq(popup.border.winid, nil) + + local hl_group = "NuiPopupTest" + + popup.border:set_highlight(hl_group) + + eq(vim.api.nvim_win_get_option(popup.winid, "winhighlight"), "FloatBorder:" .. hl_group .. ",Normal:Normal") + end) + + it("works for complex border", function() + local style = h.popup.create_border_style_map() + + local hl_group = "NuiPopupTest" + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = style, + padding = { 0 }, + }, + win_options = { + winhighlight = "Normal:Normal,FloatBorder:" .. hl_group, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + eq(vim.api.nvim_win_get_option(popup.winid, "winhighlight"), "Normal:Normal,FloatBorder:" .. hl_group) + eq(vim.api.nvim_win_get_option(popup.border.winid, "winhighlight"), "Normal:Normal,FloatBorder:" .. hl_group) + + local hl_group_override = "NuiPopupTestOverride" + + popup.border:set_highlight(hl_group_override) + + eq( + vim.api.nvim_win_get_option(popup.winid, "winhighlight"), + "FloatBorder:" .. hl_group_override .. ",Normal:Normal" + ) + eq( + vim.api.nvim_win_get_option(popup.border.winid, "winhighlight"), + "FloatBorder:" .. hl_group_override .. ",Normal:Normal" + ) + end) + end) + + describe("empty char", function() + for key, result in pairs({ + top_left = { + "─────────╮", + "│ │", + "│ │", + "╰────────╯", + }, + top = { + "╭ ╮", + "│ │", + "╰────────╯", + }, + top_right = { + "╭─────────", + "│ │", + "│ │", + "╰────────╯", + }, + right = { + "╭───────╮", + "│ ", + "│ ", + "╰───────╯", + }, + bottom_right = { + "╭────────╮", + "│ │", + "│ │", + "╰─────────", + }, + bottom = { + "╭────────╮", + "│ │", + "╰ ╯", + }, + bottom_left = { + "╭────────╮", + "│ │", + "│ │", + "─────────╯", + }, + left = { + "╭───────╮", + " │", + " │", + "╰───────╯", + }, + }) do + it("- " .. key, function() + local style = h.popup.create_border_style_map() + style[key] = "" + + popup_options = vim.tbl_deep_extend("force", popup_options, { + border = { + style = style, + text = {}, + }, + }) + + popup = Popup(popup_options) + + popup:mount() + + h.assert_buf_lines(popup.border.bufnr, result) + end) + end + end) +end) diff --git a/.config/nvim/pack/tree/start/nui.nvim/tests/nui/popup/init_spec.lua b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/popup/init_spec.lua new file mode 100644 index 0000000..fd4cad5 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/popup/init_spec.lua @@ -0,0 +1,1230 @@ +pcall(require, "luacov") + +local Popup = require("nui.popup") +local event = require("nui.utils.autocmd").event +local h = require("tests.helpers") +local spy = require("luassert.spy") + +local eq, feedkeys = h.eq, h.feedkeys + +local function percent(number, percentage) + return math.floor(number * percentage / 100) +end + +local function assert_size(popup, size, border_size) + if border_size and type(border_size) ~= "table" then + border_size = { + width = size.width + 2, + height = size.height + 2, + } + end + + local win_config = vim.api.nvim_win_get_config(popup.winid) + eq(win_config.width, size.width) + eq(win_config.height, size.height) + + if popup.border.winid then + local border_win_config = vim.api.nvim_win_get_config(popup.border.winid) + eq(border_win_config.width, border_size.width) + eq(border_win_config.height, border_size.height) + end +end + +describe("nui.popup", function() + local popup + + after_each(function() + if popup then + popup:unmount() + popup = nil + end + end) + + it("supports o.bufnr (unmanaed buffer)", function() + local bufnr = vim.api.nvim_create_buf(false, true) + + local lines = { + "a", + "b", + "c", + } + + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + + popup = Popup({ + bufnr = bufnr, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + h.assert_buf_lines(bufnr, lines) + eq(popup.bufnr, bufnr) + popup:mount() + h.assert_buf_lines(bufnr, lines) + popup:unmount() + eq(popup.bufnr, bufnr) + h.assert_buf_lines(bufnr, lines) + end) + + it("accepts number as o.ns_id", function() + local ns = "NuiPopupTest" + local ns_id = vim.api.nvim_create_namespace(ns) + + popup = Popup({ + ns_id = ns_id, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + eq(popup.ns_id, ns_id) + end) + + it("accepts string as o.ns_id", function() + local ns = "NuiPopupTest" + + popup = Popup({ + ns_id = ns, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + eq(popup.ns_id, vim.api.nvim_create_namespace(ns)) + end) + + it("uses fallback ns_id if o.ns_id=nil", function() + popup = Popup({ + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + eq(type(popup.ns_id), "number") + eq(popup.ns_id > 0, true) + end) + + describe("o.size", function() + it("supports integer", function() + popup = Popup({ + position = 0, + size = { + height = 8, + width = 20, + }, + }) + + popup:mount() + + assert_size(popup, { + height = 8, + width = 20, + }) + end) + + it("supports decimal number in (0,1)", function() + popup = Popup({ + position = 0, + size = { + height = 0.6, + width = 0.8, + }, + }) + + popup:mount() + + assert_size(popup, { + height = percent(popup._.container_info.size.height, 60), + width = percent(popup._.container_info.size.width, 80), + }) + end) + + it("supports percentage string", function() + popup = Popup({ + position = 0, + size = { + height = "60%", + width = "80%", + }, + }) + + popup:mount() + + assert_size(popup, { + height = percent(popup._.container_info.size.height, 60), + width = percent(popup._.container_info.size.width, 80), + }) + end) + end) + + h.describe_flipping_feature("lua_keymap", "method :map", function() + it("works before :mount", function() + local callback = spy.new(function() end) + + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:map("n", "l", function() + callback() + end) + + popup:mount() + + feedkeys("l", "x") + + assert.spy(callback).called() + end) + + it("works after :mount", function() + local callback = spy.new(function() end) + + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:mount() + + popup:map("n", "l", function() + callback() + end) + + feedkeys("l", "x") + + assert.spy(callback).called() + end) + + it("supports lhs table", function() + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:mount() + + popup:map("n", { "k", "l" }, "o42") + + feedkeys("k", "x") + feedkeys("l", "x") + + h.assert_buf_lines(popup.bufnr, { + "", + "42", + "42", + }) + end) + + it("supports rhs function", function() + local callback = spy.new(function() end) + + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:mount() + + popup:map("n", "l", function() + callback() + end) + + feedkeys("l", "x") + + assert.spy(callback).called() + end) + + it("supports rhs string", function() + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:mount() + + popup:map("n", "l", "o42") + + feedkeys("l", "x") + + h.assert_buf_lines(popup.bufnr, { + "", + "42", + }) + end) + + it("supports o.remap=true", function() + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:mount() + + popup:map("n", "k", "o42") + popup:map("n", "l", "k", { remap = true }) + + feedkeys("k", "x") + feedkeys("l", "x") + + h.assert_buf_lines(popup.bufnr, { + "", + "42", + "42", + }) + end) + + it("supports o.remap=false", function() + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:mount() + + popup:map("n", "k", "o42") + popup:map("n", "l", "k", { remap = false }) + + feedkeys("k", "x") + feedkeys("l", "x") + + h.assert_buf_lines(popup.bufnr, { + "", + "42", + }) + end) + + it("throws if .bufnr is nil", function() + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup.bufnr = nil + + local ok, result = pcall(function() + popup:map("n", "l", function() end) + end) + + eq(ok, false) + eq(type(string.match(result, "buffer not found")), "string") + end) + end) + + h.describe_flipping_feature("lua_keymap", "method :unmap", function() + it("works before :mount", function() + local callback = spy.new(function() end) + + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:map("n", "l", function() + callback() + end) + + popup:unmap("n", "l") + + popup:mount() + + feedkeys("l", "x") + + assert.spy(callback).not_called() + end) + + it("works after :mount", function() + local callback = spy.new(function() end) + + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:mount() + + popup:map("n", "l", function() + callback() + end) + + popup:unmap("n", "l") + + feedkeys("l", "x") + + assert.spy(callback).not_called() + end) + + it("supports lhs string", function() + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:mount() + + popup:map("n", "l", "o42") + + popup:unmap("n", "l") + + feedkeys("l", "x") + + h.assert_buf_lines(popup.bufnr, { + "", + }) + end) + + it("supports lhs table", function() + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:mount() + + popup:map("n", "k", "o42") + popup:map("n", "l", "o42") + + popup:unmap("n", { "k", "l" }) + + feedkeys("k", "x") + feedkeys("l", "x") + + h.assert_buf_lines(popup.bufnr, { + "", + }) + end) + + it("throws if .bufnr is nil", function() + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup.bufnr = nil + + local ok, result = pcall(function() + popup:unmap("n", "l") + end) + + eq(ok, false) + eq(type(string.match(result, "buffer not found")), "string") + end) + end) + + h.describe_flipping_feature("lua_autocmd", "method :on", function() + it("works before :mount", function() + local callback = spy.new(function() end) + + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:on(event.InsertEnter, function() + callback() + end) + + popup:mount() + + feedkeys("i", "x") + feedkeys("", "x") + + assert.spy(callback).called() + end) + + it("works after :mount", function() + local callback = spy.new(function() end) + + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:mount() + + popup:on(event.InsertEnter, function() + callback() + end) + + feedkeys("i", "x") + feedkeys("", "x") + + assert.spy(callback).called() + end) + + it("throws if .bufnr is nil", function() + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup.bufnr = nil + + local ok, result = pcall(function() + popup:on(event.InsertEnter, function() end) + end) + + eq(ok, false) + eq(type(string.match(result, "buffer not found")), "string") + end) + end) + + h.describe_flipping_feature("lua_autocmd", "method :off", function() + it("works before :mount", function() + local callback = spy.new(function() end) + + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:on(event.InsertEnter, function() + callback() + end) + + popup:off(event.InsertEnter) + + popup:mount() + + feedkeys("i", "x") + feedkeys("", "x") + + assert.spy(callback).not_called() + end) + + it("works after :mount", function() + local callback = spy.new(function() end) + + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup:mount() + + popup:on(event.InsertEnter, function() + callback() + end) + + popup:off(event.InsertEnter) + + feedkeys("i", "x") + feedkeys("", "x") + + assert.spy(callback).not_called() + end) + + it("throws if .bufnr is nil", function() + popup = Popup({ + enter = true, + position = "50%", + size = { + height = "60%", + width = "80%", + }, + }) + + popup.bufnr = nil + + local ok, result = pcall(function() + popup:off() + end) + + eq(ok, false) + eq(type(string.match(result, "buffer not found")), "string") + end) + end) + + describe("method :update_layout", function() + local function assert_position(position, container_winid) + container_winid = container_winid or vim.api.nvim_get_current_win() + + local win_config = vim.api.nvim_win_get_config(popup.winid) + eq(win_config.win, popup.border.winid or container_winid) + + local row, col = win_config.row, win_config.col + + if popup.border.winid then + eq(row, 1) + eq(col, 1) + + local border_win_config = vim.api.nvim_win_get_config(popup.border.winid) + local border_row, border_col = border_win_config.row, border_win_config.col + local border_width, border_height = border_win_config.width, border_win_config.height + + local delta_width = border_width - win_config.width + local delta_height = border_height - win_config.height + + eq(border_row + math.floor(delta_height / 2 + 0.5), position.row) + eq(border_col + math.floor(delta_width / 2 + 0.5), position.col) + else + eq(row, position.row) + eq(col, position.col) + end + end + + it("can change anchor (w/ simple border)", function() + popup = Popup({ + anchor = "NW", + position = "50%", + size = { width = 2, height = 1 }, + }) + + popup:mount() + + eq(popup.win_config.anchor, "NW") + + popup:update_layout({ anchor = "SW" }) + + eq(popup.win_config.anchor, "SW") + + popup:update_layout() + + eq(popup.win_config.anchor, "SW") + end) + + it("can change anchor (w/ complex border)", function() + popup = Popup({ + anchor = "NW", + border = { + style = "single", + padding = { 0 }, + }, + position = "50%", + size = { width = 2, height = 1 }, + }) + + popup:mount() + + eq(popup.win_config.anchor, nil) + eq(popup.border.win_config.anchor, "NW") + + popup:update_layout({ anchor = "SW" }) + + eq(popup.win_config.anchor, nil) + eq(popup.border.win_config.anchor, "SW") + + popup:update_layout() + + eq(popup.win_config.anchor, nil) + eq(popup.border.win_config.anchor, "SW") + end) + + it("can change size (w/ simple border)", function() + local size = { + width = 2, + height = 1, + } + + popup = Popup({ + position = "50%", + size = size, + }) + + popup:mount() + + eq(type(popup.border.winid), "nil") + + assert_size(popup, size) + + local new_size = { + width = size.width + 2, + height = size.height + 2, + } + + popup:update_layout({ size = new_size }) + + assert_size(popup, new_size) + end) + + it("can change size (w/ complex border)", function() + local hl_group = "NuiPopupTest" + local style = h.popup.create_border_style_map_with_tuple(hl_group) + + local size = { + width = 2, + height = 1, + } + + popup = Popup({ + ns_id = vim.api.nvim_create_namespace("NuiTest"), + border = { + style = style, + padding = { 0 }, + }, + position = "50%", + size = size, + }) + + popup:mount() + + eq(type(popup.border.winid), "number") + + assert_size(popup, size, true) + h.popup.assert_border_lines({ + size = size, + border = { style = style }, + }, popup.border.bufnr) + h.popup.assert_border_highlight({ + size = size, + ns_id = popup.ns_id, + }, popup.border.bufnr, hl_group) + + local new_size = { + width = size.width + 2, + height = size.height + 2, + } + + popup:update_layout({ size = new_size }) + + assert_size(popup, new_size, true) + h.popup.assert_border_lines({ + size = new_size, + border = { style = style }, + }, popup.border.bufnr) + h.popup.assert_border_highlight({ + size = new_size, + ns_id = popup.ns_id, + }, popup.border.bufnr, hl_group) + end) + + it("can change position (w/ simple border)", function() + local position = { + row = 0, + col = 0, + } + + popup = Popup({ + position = position, + size = { + width = 4, + height = 2, + }, + }) + + popup:mount() + + eq(type(popup.border.winid), "nil") + + assert_position(position) + + local new_position = { + row = position.row + 2, + col = position.col + 2, + } + + popup:update_layout({ position = new_position }) + + assert_position(new_position) + end) + + it("can change position (w/ complex border)", function() + local hl_group = "NuiPopupTest" + local style = h.popup.create_border_style_map_with_tuple(hl_group) + + local position = { + row = 0, + col = 0, + } + + popup = Popup({ + ns_id = vim.api.nvim_create_namespace("NuiTest"), + border = { + style = style, + padding = { 0 }, + }, + position = position, + size = { + width = 4, + height = 2, + }, + }) + + popup:mount() + + eq(type(popup.border.winid), "number") + + assert_position(position) + + local new_position = { + row = position.row + 2, + col = position.col + 2, + } + + popup:update_layout({ position = new_position }) + + assert_position(new_position) + end) + + it("refreshes layout if container size changes", function() + local container_size = { + width = 20, + height = 10, + } + + local container_popup = Popup({ + position = 0, + size = container_size, + }) + + container_popup:mount() + + popup = Popup({ + relative = { + type = "win", + winid = container_popup.winid, + }, + position = "20%", + size = "50%", + }) + + popup:mount() + + assert_size(popup, { + width = percent(container_size.width, 50), + height = percent(container_size.height, 50), + }) + + assert_position({ + row = percent(container_size.height - percent(container_size.height, 50), 20), + col = percent(container_size.width - percent(container_size.width, 50), 20), + }, container_popup.winid) + + container_size = { + width = 16, + height = 8, + } + + container_popup:update_layout({ + size = container_size, + }) + + popup:update_layout() + + assert_size(popup, { + width = percent(container_size.width, 50), + height = percent(container_size.height, 50), + }) + + assert_position({ + row = percent(container_size.height - percent(container_size.height, 50), 20), + col = percent(container_size.width - percent(container_size.width, 50), 20), + }, container_popup.winid) + end) + + it("throws if missing config 'relative'", function() + popup = Popup({}) + + local ok, result = pcall(function() + popup:update_layout({}) + end) + + eq(ok, false) + eq(type(string.match(result, "missing layout config: relative")), "string") + end) + + it("throws if missing config 'size'", function() + popup = Popup({}) + + local ok, result = pcall(function() + popup:update_layout({ + relative = "win", + }) + end) + + eq(ok, false) + eq(type(string.match(result, "missing layout config: size")), "string") + end) + + it("throws if missing config 'position'", function() + popup = Popup({}) + + local ok, result = pcall(function() + popup:update_layout({ + relative = "win", + size = "50%", + }) + end) + + eq(ok, false) + eq(type(string.match(result, "missing layout config: position")), "string") + end) + end) + + describe("method :mount", function() + it("throws if layout is not ready", function() + popup = Popup({}) + + local ok, result = pcall(function() + popup:mount() + end) + + eq(ok, false) + eq(type(string.match(result, "layout is not ready")), "string") + end) + + it("is idempotent", function() + popup = Popup({ + position = 0, + size = 10, + }) + + local border_mount = spy.on(popup.border, "mount") + + popup:mount() + + local bufnr, winid = popup.bufnr, popup.winid + + eq(type(bufnr), "number") + eq(type(winid), "number") + assert.spy(border_mount).was_called(1) + + popup:mount() + + eq(bufnr, popup.bufnr) + eq(winid, popup.winid) + assert.spy(border_mount).was_called(1) + end) + end) + + h.describe_flipping_feature("lua_autocmd", "method :unmount", function() + it("is called when quitted", function() + popup = Popup({ + position = 0, + size = 10, + }) + + local popup_unmount = spy.on(popup, "unmount") + + popup:mount() + + vim.api.nvim_buf_call(popup.bufnr, function() + vim.cmd([[quit]]) + end) + + vim.wait(100, function() + return not popup._.mounted + end, 10) + + assert.spy(popup_unmount).was_called() + end) + end) + + h.describe_flipping_feature("lua_autocmd", "method :hide", function() + it("works", function() + popup = Popup({ + position = 0, + size = 10, + }) + + popup:mount() + + vim.api.nvim_buf_set_lines(popup.bufnr, 0, -1, false, { + "42", + }) + + eq(type(popup.bufnr), "number") + eq(type(popup.winid), "number") + + popup:hide() + + eq(type(popup.bufnr), "number") + eq(type(popup.winid), "nil") + + h.assert_buf_lines(popup.bufnr, { + "42", + }) + end) + + it("is idempotent", function() + popup = Popup({ + position = 0, + size = 10, + }) + + popup:mount() + + local prev_winids = vim.api.nvim_list_wins() + + popup:hide() + + local curr_winids = vim.api.nvim_list_wins() + + eq(#prev_winids, #curr_winids + 1) + + popup:hide() + + eq(#curr_winids, #vim.api.nvim_list_wins()) + end) + + it("does nothing if not mounted", function() + popup = Popup({ + position = 0, + size = 10, + }) + + local prev_winids = vim.api.nvim_list_wins() + + popup:hide() + + local curr_winids = vim.api.nvim_list_wins() + + eq(#prev_winids, #curr_winids) + end) + + it("is called when window is closed", function() + popup = Popup({ + position = 0, + size = 10, + }) + + local popup_hide = spy.on(popup, "hide") + + popup:mount() + + vim.api.nvim_buf_call(popup.bufnr, function() + vim.cmd([[:bdelete]]) + end) + + assert.spy(popup_hide).was_called() + end) + + it("is not called when other popup using same buffer is hidden", function() + popup = Popup({ + position = 0, + size = 10, + }) + + local another_popup = Popup({ + bufnr = popup.bufnr, + position = 11, + size = 5, + }) + + local popup_hide = spy.on(popup, "hide") + + popup:mount() + + another_popup:mount() + another_popup:hide() + + assert.spy(popup_hide).was_not_called() + + another_popup:unmount() + end) + end) + + h.describe_flipping_feature("lua_autocmd", "method :show", function() + it("works", function() + popup = Popup({ + position = 0, + size = 10, + }) + + popup:mount() + + vim.api.nvim_buf_set_lines(popup.bufnr, 0, -1, false, { + "42", + }) + + local bufnr, winid = popup.bufnr, popup.winid + eq(type(bufnr), "number") + eq(type(winid), "number") + + popup:hide() + popup:show() + + eq(bufnr, popup.bufnr) + eq(type(popup.winid), "number") + eq(winid ~= popup.winid, true) + + h.assert_buf_lines(popup.bufnr, { + "42", + }) + end) + + it("is idempotent", function() + popup = Popup({ + position = 0, + size = 10, + }) + + popup:mount() + + popup:hide() + + local prev_winids = vim.api.nvim_list_wins() + + popup:show() + + local curr_winids = vim.api.nvim_list_wins() + + eq(#prev_winids + 1, #curr_winids) + + popup:show() + + eq(#curr_winids, #vim.api.nvim_list_wins()) + end) + + it("mounts if not mounted", function() + popup = Popup({ + position = 0, + size = 10, + }) + + local prev_winids = vim.api.nvim_list_wins() + + popup:show() + + local curr_winids = vim.api.nvim_list_wins() + + eq(#prev_winids + 1, #curr_winids) + end) + + it("does nothing if not hidden", function() + popup = Popup({ + position = 0, + size = 10, + }) + + popup:mount() + + local prev_winids = vim.api.nvim_list_wins() + + popup:show() + + local curr_winids = vim.api.nvim_list_wins() + + eq(#prev_winids, #curr_winids) + end) + + it("can show popups using the same buffer", function() + popup = Popup({ + position = 0, + size = 10, + }) + + vim.api.nvim_buf_set_lines(popup.bufnr, 0, -1, false, { + "42", + }) + + local another_popup = Popup({ + bufnr = popup.bufnr, + position = 11, + size = 5, + }) + + popup:mount() + another_popup:mount() + + local bufnr, winid = popup.bufnr, popup.winid + eq(type(bufnr), "number") + eq(type(winid), "number") + + local another_bufnr, another_winid = another_popup.bufnr, another_popup.winid + eq(type(another_bufnr), "number") + eq(type(another_winid), "number") + + eq(bufnr, another_bufnr) + + popup:hide() + another_popup:hide() + + popup:show() + another_popup:show() + + eq(bufnr, popup.bufnr) + eq(type(popup.winid), "number") + eq(winid ~= popup.winid, true) + + eq(another_bufnr, another_popup.bufnr) + eq(type(another_popup.winid), "number") + eq(another_winid ~= another_popup.winid, true) + + h.assert_buf_lines(bufnr, { + "42", + }) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/nui.nvim/tests/nui/split/init_spec.lua b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/split/init_spec.lua new file mode 100644 index 0000000..28f0275 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/split/init_spec.lua @@ -0,0 +1,1006 @@ +pcall(require, "luacov") + +local Split = require("nui.split") +local event = require("nui.utils.autocmd").event +local h = require("tests.helpers") +local spy = require("luassert.spy") + +local eq, feedkeys = h.eq, h.feedkeys + +local function percent(number, percentage) + return math.floor(number * percentage / 100) +end + +describe("nui.split", function() + local split + + before_each(function() + vim.o.winwidth = 10 + end) + + after_each(function() + split:unmount() + end) + + describe("o.enter", function() + it("if true, sets the split as current window", function() + split = Split({ + enter = true, + size = 10, + position = "bottom", + }) + + local winid = vim.api.nvim_get_current_win() + + split:mount() + + h.eq(winid ~= split.winid, true) + h.eq(split.winid, vim.api.nvim_get_current_win()) + end) + + it("if false, does not set the split as current window", function() + split = Split({ + enter = false, + size = 10, + position = "bottom", + }) + + local winid = vim.api.nvim_get_current_win() + + split:mount() + + h.eq(winid ~= split.winid, true) + h.eq(winid, vim.api.nvim_get_current_win()) + end) + + it("is true by default", function() + split = Split({ + size = 10, + position = "bottom", + }) + + local winid = vim.api.nvim_get_current_win() + + split:mount() + + h.eq(winid ~= split.winid, true) + h.eq(split.winid, vim.api.nvim_get_current_win()) + end) + end) + + describe("o.size", function() + for position, dimension in pairs({ top = "height", right = "width", bottom = "height", left = "width" }) do + it(string.format("is set as %s if o.position=%s", dimension, position), function() + local size = 20 + + split = Split({ + size = size, + position = position, + }) + + split:mount() + + local nvim_method = string.format("nvim_win_get_%s", dimension) + + eq(vim.api[nvim_method](split.winid), size) + end) + end + + it("supports table (.width)", function() + local size = 10 + + split = Split({ + size = { width = size }, + position = "left", + }) + + split:mount() + + eq(vim.api.nvim_win_get_width(split.winid), size) + end) + + it("supports table (.height)", function() + local size = 10 + + split = Split({ + size = { height = size }, + position = "top", + }) + + split:mount() + + eq(vim.api.nvim_win_get_height(split.winid), size) + end) + + it("is optional", function() + split = Split({ + position = "bottom", + }) + + split:mount() + + eq(type(vim.api.nvim_win_get_height(split.winid)), "number") + end) + + it("works with relative='editor'", function() + vim.api.nvim_set_option("showtabline", 2) + vim.api.nvim_set_option("cmdheight", 2) + + split = Split({ + size = "50%", + position = "bottom", + relative = "editor", + }) + + split:mount() + + eq(vim.api.nvim_win_get_height(split.winid), math.floor((vim.o.lines - 4) / 2)) + + vim.api.nvim_set_option("cmdheight", 1) + vim.api.nvim_set_option("showtabline", 1) + end) + end) + + describe("o.relative", function() + it("supports 'editor'", function() + local left_half_split = Split({ + size = "50%", + position = "left", + }) + + left_half_split:mount() + + split = Split({ + size = 20, + position = "bottom", + relative = "editor", + }) + + split:mount() + + eq(vim.api.nvim_win_get_width(split.winid), vim.o.columns) + + left_half_split:unmount() + end) + + it("'editor' is not tied to specific window", function() + local center_split = Split({ + size = 20, + position = "right", + }) + + center_split:mount() + + split = Split({ + size = 20, + position = "right", + relative = "editor", + }) + + split:mount() + + center_split:unmount() + + split:hide() + + split:show() + end) + + it("supports 'win'", function() + local left_half_split = Split({ + size = "50%", + position = "left", + }) + + left_half_split:mount() + + split = Split({ + size = 20, + position = "bottom", + relative = "win", + }) + + split:mount() + + eq(vim.api.nvim_win_get_width(split.winid), vim.o.columns / 2) + + left_half_split:unmount() + end) + + it("supports specific window", function() + local winid = vim.api.nvim_get_current_win() + + local left_half_split = Split({ + enter = false, + size = "30%", + position = "left", + }) + + left_half_split:mount() + + eq(winid, vim.api.nvim_get_current_win()) + + eq(vim.api.nvim_win_get_width(left_half_split.winid), vim.o.columns * 30 / 100) + + split = Split({ + enter = false, + size = 10, + position = "bottom", + relative = { + type = "win", + winid = left_half_split.winid, + }, + }) + + split:mount() + + eq(winid, vim.api.nvim_get_current_win()) + + eq(vim.api.nvim_win_get_width(split.winid), vim.o.columns * 30 / 100) + + left_half_split:unmount() + end) + end) + + describe("method :mount", function() + it("opens win if not mounted", function() + split = Split({ + size = 20, + position = "bottom", + }) + + local prev_winids = vim.api.nvim_list_wins() + + split:mount() + + local new_winids = vim.tbl_filter(function(winid) + return not vim.tbl_contains(prev_winids, winid) + end, vim.api.nvim_list_wins()) + + eq(#new_winids, 1) + end) + + it("does nothing if already mounted", function() + split = Split({ + size = 20, + position = "bottom", + }) + + split:mount() + + local prev_winids = vim.api.nvim_list_wins() + + split:mount() + + local new_winids = vim.tbl_filter(function(winid) + return not vim.tbl_contains(prev_winids, winid) + end, vim.api.nvim_list_wins()) + + eq(#new_winids, 0) + end) + + it("sets buffer after creating window", function() + local ok, winid = false, nil + + split = Split({ + size = 20, + position = "bottom", + }) + + split:on(event.BufWinEnter, function() + ok, winid = true, split.winid + end) + + split:mount() + + eq(type(split.winid), "number") + + eq(ok, true) + eq(winid, split.winid) + end) + end) + + h.describe_flipping_feature("lua_autocmd", "method :unmount", function() + it("closes win if mounted", function() + split = Split({ + size = 20, + position = "bottom", + }) + + split:mount() + + local split_winid = split.winid + + local prev_winids = vim.api.nvim_list_wins() + + split:unmount() + + local curr_winids = vim.api.nvim_list_wins() + local closed_winids = vim.tbl_filter(function(winid) + return not vim.tbl_contains(curr_winids, winid) + end, prev_winids) + + eq(#closed_winids, 1) + eq(closed_winids[1], split_winid) + end) + + it("does nothing if already unmounted", function() + split = Split({ + size = 20, + position = "bottom", + }) + + local prev_winids = vim.api.nvim_list_wins() + + split:unmount() + + local curr_winids = vim.api.nvim_list_wins() + local closed_winids = vim.tbl_filter(function(winid) + return not vim.tbl_contains(curr_winids, winid) + end, prev_winids) + + eq(#closed_winids, 0) + end) + + it("is called when quitted", function() + split = Split({ + size = 10, + position = "bottom", + }) + + local split_unmount = spy.on(split, "unmount") + + split:mount() + + vim.api.nvim_buf_call(split.bufnr, function() + vim.cmd([[quit]]) + end) + + vim.wait(100, function() + return not split._.mounted + end, 10) + + assert.spy(split_unmount).was_called() + end) + end) + + h.describe_flipping_feature("lua_autocmd", "method :hide", function() + it("works", function() + local winid = vim.api.nvim_get_current_win() + + local win_height = vim.api.nvim_win_get_height(winid) + + split = Split({ + size = 20, + position = "bottom", + }) + + split:mount() + + vim.api.nvim_buf_set_lines(split.bufnr, 0, -1, false, { + "42", + }) + + eq(vim.api.nvim_win_get_height(winid) < win_height, true) + + split:hide() + + h.assert_buf_lines(split.bufnr, { + "42", + }) + + eq(vim.api.nvim_win_get_height(winid) == win_height, true) + end) + + it("is idempotent", function() + split = Split({ + size = 20, + position = "bottom", + }) + + split:mount() + + local prev_winids = vim.api.nvim_list_wins() + + split:hide() + + local curr_winids = vim.api.nvim_list_wins() + + eq(#prev_winids, #curr_winids + 1) + + split:hide() + + eq(#curr_winids, #vim.api.nvim_list_wins()) + end) + + it("does nothing if not mounted", function() + split = Split({ + size = 20, + position = "bottom", + }) + + local prev_winids = vim.api.nvim_list_wins() + + split:hide() + + local curr_winids = vim.api.nvim_list_wins() + + eq(#prev_winids, #curr_winids) + end) + + it("is called when window is closed", function() + split = Split({ + size = 20, + position = "bottom", + }) + + local split_hide = spy.on(split, "hide") + + split:mount() + + vim.api.nvim_buf_call(split.bufnr, function() + vim.cmd([[:bdelete]]) + end) + + assert.spy(split_hide).was_called() + end) + end) + + describe("method :show", function() + it("works", function() + local winid = vim.api.nvim_get_current_win() + + split = Split({ + size = 20, + position = "bottom", + }) + + split:mount() + + vim.api.nvim_buf_set_lines(split.bufnr, 0, -1, false, { + "42", + }) + + local win_height = vim.api.nvim_win_get_height(winid) + + split:hide() + split:show() + + h.assert_buf_lines(split.bufnr, { + "42", + }) + + eq(vim.api.nvim_win_get_height(winid) == win_height, true) + end) + + it("is idempotent", function() + split = Split({ + size = 20, + position = "bottom", + }) + + split:mount() + + split:hide() + + local prev_winids = vim.api.nvim_list_wins() + + split:show() + + local curr_winids = vim.api.nvim_list_wins() + + eq(#prev_winids + 1, #curr_winids) + + split:show() + + eq(#curr_winids, #vim.api.nvim_list_wins()) + end) + + it("mounts if not mounted", function() + split = Split({ + size = 20, + position = "bottom", + }) + + local prev_winids = vim.api.nvim_list_wins() + + split:show() + + local curr_winids = vim.api.nvim_list_wins() + + eq(#prev_winids + 1, #curr_winids) + end) + + it("does nothing if not hidden", function() + split = Split({ + size = 20, + position = "bottom", + }) + + split:mount() + + local prev_winids = vim.api.nvim_list_wins() + + split:show() + + local curr_winids = vim.api.nvim_list_wins() + + eq(#prev_winids, #curr_winids) + end) + end) + + describe("method :update_layout", function() + it("can change size", function() + split = Split({ positon = "bottom", size = 10 }) + + split:mount() + + eq(vim.api.nvim_win_get_height(split.winid), 10) + + split:update_layout({ size = 20 }) + + eq(vim.api.nvim_win_get_height(split.winid), 20) + end) + + it('accepts size=""', function() + split = Split({ positon = "bottom", size = 10 }) + + split:mount() + + eq(vim.api.nvim_win_get_height(split.winid), 10) + + split:update_layout({ size = "" }) + + eq(vim.api.nvim_win_get_height(split.winid), 10) + end) + + it("can change position", function() + local winid = vim.api.nvim_get_current_win() + + split = Split({ position = "bottom", size = 10 }) + + split:mount() + + eq(vim.fn.winlayout(), { + "col", + { + { "leaf", winid }, + { "leaf", split.winid }, + }, + }) + + split:update_layout({ position = "right" }) + + eq(vim.fn.winlayout(), { + "row", + { + { "leaf", winid }, + { "leaf", split.winid }, + }, + }) + end) + + it("can change position and size", function() + local winid = vim.api.nvim_get_current_win() + + split = Split({ position = "top", size = 10 }) + + split:mount() + + eq(vim.api.nvim_win_get_height(split.winid), 10) + eq(vim.fn.winlayout(), { + "col", + { + { "leaf", split.winid }, + { "leaf", winid }, + }, + }) + + split:update_layout({ position = "left", size = 20 }) + + eq(vim.api.nvim_win_get_width(split.winid), 20) + eq(vim.fn.winlayout(), { + "row", + { + { "leaf", split.winid }, + { "leaf", winid }, + }, + }) + end) + + it("can change relative", function() + local winid_one = vim.api.nvim_get_current_win() + local split_two = Split({ position = "right", size = 10 }) + split_two:mount() + + split = Split({ relative = "win", position = "top", size = 10 }) + + split:mount() + + eq(vim.api.nvim_win_get_height(split.winid), 10) + eq(vim.fn.winlayout(), { + "row", + { + { "leaf", winid_one }, + { + "col", + { + { "leaf", split.winid }, + { "leaf", split_two.winid }, + }, + }, + }, + }) + + split:update_layout({ position = "bottom", relative = "editor", size = "50%" }) + + eq(vim.api.nvim_win_get_height(split.winid), percent(vim.o.lines - 1, 50)) + eq(vim.fn.winlayout(), { + "col", + { + { + "row", + { + { "leaf", winid_one }, + { "leaf", split_two.winid }, + }, + }, + { "leaf", split.winid }, + }, + }) + + split_two:unmount() + end) + end) + + h.describe_flipping_feature("lua_keymap", "method :map", function() + it("works before :mount", function() + local callback = spy.new(function() end) + + split = Split({ + size = 20, + }) + + split:map("n", "l", function() + callback() + end) + + split:mount() + + feedkeys("l", "x") + + assert.spy(callback).called() + end) + + it("works after :mount", function() + local callback = spy.new(function() end) + + split = Split({ + size = 20, + }) + + split:mount() + + split:map("n", "l", function() + callback() + end) + + feedkeys("l", "x") + + assert.spy(callback).called() + end) + + it("supports lhs table", function() + split = Split({ + size = 20, + }) + + split:mount() + + split:map("n", { "k", "l" }, "o42") + + feedkeys("k", "x") + feedkeys("l", "x") + + h.assert_buf_lines(split.bufnr, { + "", + "42", + "42", + }) + end) + + it("supports rhs function", function() + local callback = spy.new(function() end) + + split = Split({ + size = 20, + }) + + split:mount() + + split:map("n", "l", function() + callback() + end) + + feedkeys("l", "x") + + assert.spy(callback).called() + end) + + it("supports rhs string", function() + split = Split({ + size = 20, + }) + + split:mount() + + split:map("n", "l", "o42") + + feedkeys("l", "x") + + h.assert_buf_lines(split.bufnr, { + "", + "42", + }) + end) + + it("supports o.remap=true", function() + split = Split({ + size = 20, + }) + + split:mount() + + split:map("n", "k", "o42") + split:map("n", "l", "k", { remap = true }) + + feedkeys("k", "x") + feedkeys("l", "x") + + h.assert_buf_lines(split.bufnr, { + "", + "42", + "42", + }) + end) + + it("supports o.remap=false", function() + split = Split({ + size = 20, + }) + + split:mount() + + split:map("n", "k", "o42") + split:map("n", "l", "k", { remap = false }) + + feedkeys("k", "x") + feedkeys("l", "x") + + h.assert_buf_lines(split.bufnr, { + "", + "42", + }) + end) + + it("throws if .bufnr is nil", function() + split = Split({ + size = 20, + }) + + split.bufnr = nil + + local ok, result = pcall(function() + split:map("n", "k", "o42") + end) + + eq(ok, false) + eq(type(string.match(result, "buffer not found")), "string") + end) + end) + + h.describe_flipping_feature("lua_keymap", "method :unmap", function() + it("works before :mount", function() + split = Split({ + size = 20, + }) + + split:map("n", "l", "o42") + + split:unmap("n", "l") + + split:mount() + + feedkeys("l", "x") + + h.assert_buf_lines(split.bufnr, { + "", + }) + end) + + it("works after :mount", function() + split = Split({ + size = 20, + }) + + split:mount() + + split:map("n", "l", "o42") + + split:unmap("n", "l") + + feedkeys("l", "x") + + h.assert_buf_lines(split.bufnr, { + "", + }) + end) + + it("supports lhs string", function() + split = Split({ + size = 20, + }) + + split:mount() + + split:map("n", "l", "o42") + + split:unmap("n", "l") + + feedkeys("l", "x") + + h.assert_buf_lines(split.bufnr, { + "", + }) + end) + + it("supports lhs table", function() + split = Split({ + size = 20, + }) + + split:mount() + + split:map("n", "k", "o42") + split:map("n", "l", "o42") + + split:unmap("n", { "k", "l" }) + + feedkeys("k", "x") + feedkeys("l", "x") + + h.assert_buf_lines(split.bufnr, { + "", + }) + end) + + it("throws if .bufnr is nil", function() + split = Split({ + size = 20, + }) + + split.bufnr = nil + + local ok, result = pcall(function() + split:unmap("n", "l") + end) + + eq(ok, false) + eq(type(string.match(result, "buffer not found")), "string") + end) + end) + + h.describe_flipping_feature("lua_autocmd", "method :on", function() + it("works before :mount", function() + local callback = spy.new(function() end) + + split = Split({ + size = 20, + }) + + split:on(event.InsertEnter, function() + callback() + end) + + split:mount() + + feedkeys("i", "x") + feedkeys("", "x") + + assert.spy(callback).called() + end) + + it("works after :mount", function() + local callback = spy.new(function() end) + + split = Split({ + size = 20, + }) + + split:mount() + + split:on(event.InsertEnter, function() + callback() + end) + + feedkeys("i", "x") + feedkeys("", "x") + + assert.spy(callback).called() + end) + + it("throws if .bufnr is nil", function() + split = Split({ + size = 20, + }) + + split.bufnr = nil + + local ok, result = pcall(function() + split:on(event.InsertEnter, function() end) + end) + + eq(ok, false) + eq(type(string.match(result, "buffer not found")), "string") + end) + end) + + h.describe_flipping_feature("lua_autocmd", "method :off", function() + it("works before :mount", function() + local callback = spy.new(function() end) + + split = Split({ + size = 20, + }) + + split:on(event.InsertEnter, function() + callback() + end) + + split:off(event.InsertEnter) + + split:mount() + + feedkeys("i", "x") + feedkeys("", "x") + + assert.spy(callback).not_called() + end) + + it("works after :mount", function() + local callback = spy.new(function() end) + + split = Split({ + size = 20, + }) + + split:mount() + + split:on(event.InsertEnter, function() + callback() + end) + + split:off(event.InsertEnter) + + feedkeys("i", "x") + feedkeys("", "x") + + assert.spy(callback).not_called() + end) + + it("throws if .bufnr is nil", function() + split = Split({ + size = 20, + }) + + split.bufnr = nil + + local ok, result = pcall(function() + split:off() + end) + + eq(ok, false) + eq(type(string.match(result, "buffer not found")), "string") + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/nui.nvim/tests/nui/table/init_spec.lua b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/table/init_spec.lua new file mode 100644 index 0000000..7b806c2 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/table/init_spec.lua @@ -0,0 +1,632 @@ +pcall(require, "luacov") + +local Line = require("nui.line") +local Table = require("nui.table") +local Text = require("nui.text") +local h = require("tests.helpers") + +local eq = h.eq + +describe("nui.table", function() + ---@type number, number + local winid, bufnr + + before_each(function() + winid = vim.api.nvim_get_current_win() + bufnr = vim.api.nvim_create_buf(false, true) + + vim.api.nvim_win_set_buf(winid, bufnr) + end) + + after_each(function() + vim.api.nvim_buf_delete(bufnr, { force = true }) + end) + + describe("o.bufnr", function() + it("throws if missing", function() + local ok, err = pcall(Table, {}) + eq(ok, false) + eq(type(string.match(err, "missing bufnr")), "string") + end) + + it("throws if invalid", function() + local ok, err = pcall(Table, { bufnr = 999 }) + eq(ok, false) + eq(type(string.match(err, "invalid bufnr ")), "string") + end) + + it("sets t.bufnr properly", function() + local table = Table({ bufnr = bufnr }) + + eq(table.bufnr, bufnr) + end) + end) + + describe("o.buf_options", function() + it("sets default buf options emulating scratch-buffer", function() + local table = Table({ bufnr = bufnr }) + + h.assert_buf_options(table.bufnr, { + bufhidden = "hide", + buflisted = false, + buftype = "nofile", + swapfile = false, + }) + end) + + it("locks buffer by default", function() + local table = Table({ bufnr = bufnr }) + + h.assert_buf_options(table.bufnr, { + modifiable = false, + readonly = true, + undolevels = 0, + }) + end) + + it("sets values", function() + local table = Table({ + bufnr = bufnr, + buf_options = { + undolevels = -1, + }, + }) + + h.assert_buf_options(table.bufnr, { + undolevels = -1, + }) + end) + end) + + describe("o.ns_id", function() + it("sets t.ns_id if o.ns_id is string", function() + local ns = "NuiTest" + local table = Table({ bufnr = bufnr, ns_id = ns }) + + local namespaces = vim.api.nvim_get_namespaces() + + eq(table.ns_id, namespaces[ns]) + end) + + it("sets t.ns_id if o.ns_id is number", function() + local ns = "NuiTest" + local ns_id = vim.api.nvim_create_namespace(ns) + local table = Table({ bufnr = bufnr, ns_id = ns_id }) + + eq(table.ns_id, ns_id) + end) + end) + + describe("o.columns", function() + describe(".id", function() + it("fallbacks t o .accessor_key", function() + local table = Table({ + bufnr = bufnr, + columns = { { accessor_key = "ID" } }, + data = { { ID = 42 } }, + }) + + table:render() + + vim.api.nvim_win_set_cursor(winid, { 2, 3 }) + + eq(table:get_cell().column.id, "ID") + end) + + for header_type, header in pairs({ + string = "ID", + NuiText = Text("ID"), + NuiLine = Line({ Text("I"), Text("D") }), + }) do + it(string.format("fallbacks to .header (%s)", header_type), function() + local table = Table({ + bufnr = bufnr, + columns = { + { + header = header, + accessor_fn = function() + return "" + end, + }, + }, + data = { {} }, + }) + + table:render() + + vim.api.nvim_win_set_cursor(winid, { 4, 3 }) + + eq(table:get_cell().column.id, "ID") + end) + end + + it("throws if missing", function() + local ok, err = pcall(function() + return Table({ + bufnr = bufnr, + columns = { {} }, + }) + end) + eq(ok, false) + eq(type(string.match(err, "missing column id")), "string") + end) + end) + end) + + describe("method :render", function() + local columns + local data + + before_each(function() + columns = { + { + header = "First Name", + accessor_key = "firstName", + footer = "firstName", + }, + { + header = "Last Name", + accessor_key = "lastName", + footer = "lastName", + }, + } + + data = { + { + firstName = "tanner", + lastName = "linsley", + age = 24, + visits = 100, + status = "In Relationship", + progress = 50, + }, + { + firstName = "tandy", + lastName = "miller", + age = 40, + visits = 40, + status = "Single", + progress = 80, + }, + { + firstName = "joe", + lastName = "dirte", + age = 45, + visits = 20, + status = "Complicated", + progress = 10, + }, + } + end) + + it("can handle empty columns", function() + local table = Table({ + bufnr = bufnr, + data = data, + }) + table:render() + h.assert_buf_lines(table.bufnr, { "" }) + end) + + it("can handle empty data", function() + local table = Table({ + bufnr = bufnr, + columns = { + { + accessor_key = "firstName", + }, + }, + }) + table:render() + h.assert_buf_lines(table.bufnr, { "" }) + end) + + it("can handle empty columns and data", function() + local table = Table({ bufnr = bufnr }) + table:render() + h.assert_buf_lines(table.bufnr, { "" }) + end) + + it("works w/ header w/ footer", function() + local table = Table({ + bufnr = bufnr, + columns = columns, + data = data, + }) + + table:render() + + h.assert_buf_lines(table.bufnr, { + "┌──────────┬─────────┐", + "│First Name│Last Name│", + "├──────────┼─────────┤", + "│tanner │linsley │", + "├──────────┼─────────┤", + "│tandy │miller │", + "├──────────┼─────────┤", + "│joe │dirte │", + "├──────────┼─────────┤", + "│firstName │lastName │", + "└──────────┴─────────┘", + }) + end) + + it("works w/ header w/o footer", function() + for _, column in ipairs(columns) do + column.align = "center" + column.footer = nil + end + + local table = Table({ + bufnr = bufnr, + columns = columns, + data = data, + }) + + table:render() + + h.assert_buf_lines(table.bufnr, { + "┌──────────┬─────────┐", + "│First Name│Last Name│", + "├──────────┼─────────┤", + "│ tanner │ linsley │", + "├──────────┼─────────┤", + "│ tandy │ miller │", + "├──────────┼─────────┤", + "│ joe │ dirte │", + "└──────────┴─────────┘", + }) + end) + + it("works w/o header w/ footer", function() + for _, column in ipairs(columns) do + column.header = nil + end + + local table = Table({ + bufnr = bufnr, + columns = columns, + data = data, + }) + + table:render() + + h.assert_buf_lines(table.bufnr, { + "┌─────────┬────────┐", + "│tanner │linsley │", + "├─────────┼────────┤", + "│tandy │miller │", + "├─────────┼────────┤", + "│joe │dirte │", + "├─────────┼────────┤", + "│firstName│lastName│", + "└─────────┴────────┘", + }) + end) + + it("works w/o header w/o footer", function() + for _, column in ipairs(columns) do + column.header = nil + column.footer = nil + end + + local table = Table({ + bufnr = bufnr, + columns = columns, + data = data, + }) + + table:render() + + h.assert_buf_lines(table.bufnr, { + "┌──────┬───────┐", + "│tanner│linsley│", + "├──────┼───────┤", + "│tandy │miller │", + "├──────┼───────┤", + "│joe │dirte │", + "└──────┴───────┘", + }) + end) + + it("supports param linenr_start", function() + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + "START: NuiTest", + "", + "END: NuiTest", + }) + + local table = Table({ + bufnr = bufnr, + columns = columns, + data = { data[1] }, + }) + + table:render(2) + h.assert_buf_lines(table.bufnr, { + "START: NuiTest", + "┌──────────┬─────────┐", + "│First Name│Last Name│", + "├──────────┼─────────┤", + "│tanner │linsley │", + "├──────────┼─────────┤", + "│firstName │lastName │", + "└──────────┴─────────┘", + "END: NuiTest", + }) + + table:render(4) + h.assert_buf_lines(table.bufnr, { + "START: NuiTest", + "", + "", + "┌──────────┬─────────┐", + "│First Name│Last Name│", + "├──────────┼─────────┤", + "│tanner │linsley │", + "├──────────┼─────────┤", + "│firstName │lastName │", + "└──────────┴─────────┘", + "END: NuiTest", + }) + + table:render(3) + h.assert_buf_lines(table.bufnr, { + "START: NuiTest", + "", + "┌──────────┬─────────┐", + "│First Name│Last Name│", + "├──────────┼─────────┤", + "│tanner │linsley │", + "├──────────┼─────────┤", + "│firstName │lastName │", + "└──────────┴─────────┘", + "END: NuiTest", + }) + end) + + describe("grouped columns", function() + local grouped_columns + before_each(function() + grouped_columns = { + { + header = "Name", + footer = function(info) + return info.column.id + end, + columns = { + { + accessor_key = "firstName", + footer = "firstName", + }, + { + id = "lastName", + header = "Last Name", + accessor_key = "lastName", + footer = function(info) + return info.column.id + end, + }, + }, + }, + { + header = "Info", + footer = function(info) + return info.column.id + end, + columns = { + { + header = "Age", + accessor_key = "age", + footer = "age", + }, + { + header = "More Info", + footer = function(info) + return info.column.id + end, + columns = { + { + accessor_key = "visits", + header = "Visits", + footer = function(info) + return info.column.id + end, + }, + { + accessor_key = "status", + header = "Status", + footer = function(info) + return info.column.id + end, + }, + }, + }, + }, + }, + { + header = "Profile Progress", + accessor_key = "progress", + footer = function(info) + return info.column.id + end, + }, + } + end) + + it("is drawn correctly", function() + local table = Table({ + bufnr = bufnr, + columns = grouped_columns, + data = data, + }) + + table:render() + + h.assert_buf_lines(table.bufnr, { + "┌───────────────────┬──────────────────────────┬────────────────┐", + "│Name │Info │ │", + "├─────────┬─────────┼───┬──────────────────────┤ │", + "│ │ │ │More Info │ │", + "│ │ │ ├──────┬───────────────┤ │", + "│firstName│Last Name│Age│Visits│Status │Profile Progress│", + "├─────────┼─────────┼───┼──────┼───────────────┼────────────────┤", + "│tanner │linsley │24 │100 │In Relationship│50 │", + "├─────────┼─────────┼───┼──────┼───────────────┼────────────────┤", + "│tandy │miller │40 │40 │Single │80 │", + "├─────────┼─────────┼───┼──────┼───────────────┼────────────────┤", + "│joe │dirte │45 │20 │Complicated │10 │", + "├─────────┼─────────┼───┼──────┼───────────────┼────────────────┤", + "│firstName│lastName │age│visits│status │progress │", + "│ │ │ ├──────┴───────────────┤ │", + "│ │ │ │More Info │ │", + "├─────────┴─────────┼───┴──────────────────────┤ │", + "│Name │Info │ │", + "└───────────────────┴──────────────────────────┴────────────────┘", + }) + end) + end) + end) + + describe("method :get_cell", function() + it("returns nil on border", function() + local table = Table({ + bufnr = bufnr, + columns = { { accessor_key = "value" } }, + data = { { value = "Such Value!" } }, + }) + + table:render() + + vim.api.nvim_win_set_cursor(winid, { 1, 5 }) + + local cell = table:get_cell() + + eq(cell, nil) + end) + + it("works after shifting", function() + local table = Table({ + bufnr = bufnr, + columns = { { accessor_key = "value" } }, + data = { { id = 0, value = "Such Value!" } }, + }) + + table:render() + + local cell + + vim.api.nvim_win_set_cursor(winid, { 2, 5 }) + cell = table:get_cell() + eq(type(cell), "table") + eq(cell.row.original.id, 0) + + table:render(2) + + vim.api.nvim_win_set_cursor(winid, { 2, 5 }) + cell = table:get_cell() + eq(type(cell), "nil") + + vim.api.nvim_win_set_cursor(winid, { 3, 5 }) + cell = table:get_cell() + eq(type(cell), "table") + eq(cell.row.original.id, 0) + end) + + it("can take position", function() + local table = Table({ + bufnr = bufnr, + columns = { + { accessor_key = "id" }, + { accessor_key = "value" }, + }, + data = { + { id = 1, value = "One" }, + { id = 2, value = "Two" }, + }, + }) + + table:render() + + local cell + + vim.api.nvim_win_set_cursor(winid, { 2, 3 }) + cell = table:get_cell() + eq(cell.get_value(), 1) + + cell = table:get_cell({ 1, 1 }) + eq(cell.get_value(), "Two") + end) + end) + + describe("method :refresh_cell", function() + it("can truncate NuiText on refesh", function() + local table = Table({ + bufnr = bufnr, + columns = { { accessor_key = "value" } }, + data = { { value = "Such Value!" } }, + }) + + table:render() + + h.assert_buf_lines(table.bufnr, { + "┌───────────┐", + "│Such Value!│", + "└───────────┘", + }) + + vim.api.nvim_win_set_cursor(winid, { 2, 5 }) + + local cell = table:get_cell() + + cell.row.original.value = "Such Looooooog Value!" + + table:refresh_cell(cell) + + h.assert_buf_lines(table.bufnr, { + "┌───────────┐", + "│Such Loooo…│", + "└───────────┘", + }) + end) + + it("can truncate NuiLine on refesh", function() + local table = Table({ + bufnr = bufnr, + columns = { + { + accessor_key = "value", + cell = function(cell) + return Line({ Text(tostring(cell.get_value()), "NuiTest"), Text(" years old") }) + end, + }, + }, + data = { { value = 42 } }, + }) + + table:render() + + h.assert_buf_lines(table.bufnr, { + "┌────────────┐", + "│42 years old│", + "└────────────┘", + }) + + vim.api.nvim_win_set_cursor(winid, { 2, 5 }) + + local cell = table:get_cell() + + eq(type(cell), "table") + + cell.row.original.value = 100 + + table:refresh_cell(cell) + + h.assert_buf_lines(table.bufnr, { + "┌────────────┐", + "│100 years o…│", + "└────────────┘", + }) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/nui.nvim/tests/nui/text/init_spec.lua b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/text/init_spec.lua new file mode 100644 index 0000000..b41b2a5 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/text/init_spec.lua @@ -0,0 +1,284 @@ +pcall(require, "luacov") + +local Text = require("nui.text") +local h = require("tests.helpers") +local spy = require("luassert.spy") + +local eq, tbl_omit = h.eq, h.tbl_omit + +describe("nui.text", function() + local multibyte_char + + before_each(function() + multibyte_char = "║" + end) + + it("can clone nui.text object", function() + local hl_group = "NuiTextTest" + + local t1 = Text("42", hl_group) + + t1.extmark.id = 42 + local t2 = Text(t1) + eq(t2:content(), t1:content()) + eq(t2.extmark, tbl_omit(t1.extmark, { "id" })) + + t2.extmark.id = 42 + local t3 = Text(t2) + eq(t3:content(), t2:content()) + eq(t3.extmark, tbl_omit(t2.extmark, { "id" })) + end) + + it("can clone nui.text object overriding extmark", function() + local hl_group = "NuiTextTest" + local hl_group_override = "NuiTextTestOverride" + + local t1 = Text("42", hl_group) + + t1.extmark.id = 42 + local t2 = Text(t1, hl_group_override) + eq(t2:content(), t1:content()) + eq(t2.extmark, { hl_group = hl_group_override }) + + local t3 = Text(t2, { id = 42, hl_group = hl_group }) + eq(t3:content(), t2:content()) + eq(t3.extmark, { hl_group = hl_group }) + end) + + describe("method :set", function() + it("works", function() + local hl_group = "NuiTextTest" + local hl_group_override = "NuiTextTestOverride" + + local text = Text("42", hl_group) + + eq(text:content(), "42") + eq(text:length(), 2) + eq(text.extmark, { + hl_group = hl_group, + }) + + text.extmark.id = 42 + + text:set("3") + eq(text:content(), "3") + eq(text:length(), 1) + eq(text.extmark, { + hl_group = hl_group, + id = 42, + }) + + text:set("9", hl_group_override) + eq(text:content(), "9") + eq(text.extmark, { + hl_group = hl_group_override, + id = 42, + }) + + text:set("11", { hl_group = hl_group }) + eq(text:content(), "11") + eq(text.extmark, { + hl_group = hl_group, + id = 42, + }) + + text.extmark.id = nil + + text:set("42", { id = 42, hl_group = hl_group }) + eq(text:content(), "42") + eq(text.extmark, { hl_group = hl_group }) + end) + end) + + describe("method :content", function() + it("works", function() + local content = "42" + local text = Text(content) + eq(text:content(), content) + + local multibyte_content = multibyte_char + local multibyte_text = Text(multibyte_content) + eq(multibyte_text:content(), multibyte_content) + end) + end) + + describe("method :length", function() + it("works", function() + local content = "42" + local text = Text(content) + eq(text:length(), 2) + eq(text:length(), vim.fn.strlen(content)) + + local multibyte_content = multibyte_char + local multibyte_text = Text(multibyte_content) + eq(multibyte_text:length(), 3) + eq(multibyte_text:length(), vim.fn.strlen(multibyte_content)) + end) + end) + + describe("method :width", function() + it("works", function() + local content = "42" + local text = Text(content) + eq(text:width(), 2) + eq(text:width(), vim.fn.strwidth(content)) + + local multibyte_content = multibyte_char + local multibyte_text = Text(multibyte_content) + eq(multibyte_text:width(), 1) + eq(multibyte_text:width(), vim.fn.strwidth(multibyte_content)) + end) + end) + + describe("method", function() + local winid, bufnr + local initial_lines + + before_each(function() + winid = vim.api.nvim_get_current_win() + bufnr = vim.api.nvim_create_buf(false, true) + + vim.api.nvim_win_set_buf(winid, bufnr) + + initial_lines = { " 1", multibyte_char .. " 2", " 3" } + end) + + after_each(function() + vim.api.nvim_buf_delete(bufnr, { force = true }) + end) + + local function reset_lines(lines) + initial_lines = lines or initial_lines + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, initial_lines) + end + + describe(":highlight", function() + local hl_group, ns, ns_id + local linenr, byte_start + local text + + before_each(function() + hl_group = "NuiTextTest" + ns = "NuiTest" + ns_id = vim.api.nvim_create_namespace(ns) + end) + + it("is applied with :render", function() + reset_lines() + linenr, byte_start = 1, 0 + text = Text("a", hl_group) + text:render(bufnr, ns_id, linenr, byte_start) + h.assert_highlight(bufnr, ns_id, linenr, text:content(), hl_group) + end) + + it("is applied with :render_char", function() + reset_lines() + linenr, byte_start = 1, 0 + text = Text(multibyte_char, hl_group) + text:render_char(bufnr, ns_id, linenr, byte_start) + h.assert_highlight(bufnr, ns_id, linenr, text:content(), hl_group) + end) + + it("can highlight existing buffer text", function() + reset_lines() + linenr, byte_start = 2, 0 + text = Text(initial_lines[linenr], hl_group) + text:highlight(bufnr, ns_id, linenr, byte_start) + h.assert_highlight(bufnr, ns_id, linenr, text:content(), hl_group) + end) + + it("does not create multiple extmarks", function() + reset_lines() + linenr, byte_start = 2, 0 + text = Text(initial_lines[linenr], hl_group) + + text:highlight(bufnr, ns_id, linenr, byte_start) + h.assert_highlight(bufnr, ns_id, linenr, text:content(), hl_group) + text:highlight(bufnr, ns_id, linenr, byte_start) + h.assert_highlight(bufnr, ns_id, linenr, text:content(), hl_group) + text:highlight(bufnr, ns_id, linenr, byte_start) + h.assert_highlight(bufnr, ns_id, linenr, text:content(), hl_group) + end) + end) + + describe(":render", function() + it("works on line with singlebyte characters", function() + reset_lines() + + local text = Text("a") + + spy.on(text, "highlight") + + text:render(bufnr, -1, 1, 1) + + assert.spy(text.highlight).was_called(1) + assert.spy(text.highlight).was_called_with(text, bufnr, -1, 1, 1) + + h.assert_buf_lines(bufnr, { + " a1", + initial_lines[2], + initial_lines[3], + }) + end) + + it("works on line with multibyte characters", function() + reset_lines() + + local text = Text("a") + + spy.on(text, "highlight") + + text:render(bufnr, -1, 2, vim.fn.strlen(multibyte_char)) + + assert.spy(text.highlight).was_called(1) + assert.spy(text.highlight).was_called_with(text, bufnr, -1, 2, vim.fn.strlen(multibyte_char)) + + h.assert_buf_lines(bufnr, { + initial_lines[1], + multibyte_char .. "a2", + initial_lines[3], + }) + end) + end) + + describe(":render_char", function() + it("works on line with singlebyte characters", function() + reset_lines() + + local text = Text("a") + + spy.on(text, "highlight") + + text:render_char(bufnr, -1, 1, 1) + + assert.spy(text.highlight).was_called(1) + assert.spy(text.highlight).was_called_with(text, bufnr, -1, 1, 1) + + h.assert_buf_lines(bufnr, { + " a1", + initial_lines[2], + initial_lines[3], + }) + end) + + it("works on line with multibyte characters", function() + reset_lines() + + local text = Text("a") + + spy.on(text, "highlight") + + text:render_char(bufnr, -1, 2, 1) + + assert.spy(text.highlight).was_called(1) + assert.spy(text.highlight).was_called_with(text, bufnr, -1, 2, vim.fn.strlen(multibyte_char)) + + h.assert_buf_lines(bufnr, { + initial_lines[1], + multibyte_char .. "a2", + initial_lines[3], + }) + end) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/nui.nvim/tests/nui/tree/init_spec.lua b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/tree/init_spec.lua new file mode 100644 index 0000000..5358076 --- /dev/null +++ b/.config/nvim/pack/tree/start/nui.nvim/tests/nui/tree/init_spec.lua @@ -0,0 +1,912 @@ +pcall(require, "luacov") + +local Text = require("nui.text") +local Tree = require("nui.tree") +local h = require("tests.helpers") + +local eq = h.eq + +describe("nui.tree", function() + local winid, bufnr + + before_each(function() + winid = vim.api.nvim_get_current_win() + bufnr = vim.api.nvim_create_buf(false, true) + + vim.api.nvim_win_set_buf(winid, bufnr) + end) + + after_each(function() + vim.api.nvim_buf_delete(bufnr, { force = true }) + end) + + describe("(#deprecated) o.winid", function() + it("throws if missing", function() + local ok, err = pcall(function() + return Tree({}) + end) + eq(ok, false) + eq(type(string.match(err, "missing bufnr")), "string") + end) + + it("throws if invalid", function() + local ok, err = pcall(function() + return Tree({ winid = 999 }) + end) + eq(ok, false) + eq(type(string.match(err, "invalid winid ")), "string") + end) + + it("sets t.winid and t.bufnr properly", function() + local tree = Tree({ winid = winid }) + + eq(tree.winid, winid) + eq(tree.bufnr, bufnr) + end) + end) + + describe("o.bufnr", function() + it("throws if missing", function() + local ok, err = pcall(function() + return Tree({}) + end) + eq(ok, false) + eq(type(string.match(err, "missing bufnr")), "string") + end) + + it("throws if invalid", function() + local ok, err = pcall(function() + return Tree({ bufnr = 999 }) + end) + eq(ok, false) + eq(type(string.match(err, "invalid bufnr ")), "string") + end) + + it("sets t.bufnr properly", function() + local tree = Tree({ bufnr = bufnr }) + + eq(tree.winid, nil) + eq(tree.bufnr, bufnr) + end) + end) + + it("throws on duplicated node id", function() + local ok, err = pcall(function() + return Tree({ + bufnr = bufnr, + nodes = { + Tree.Node({ id = "id", text = "text" }), + Tree.Node({ id = "id", text = "text" }), + }, + }) + end) + eq(ok, false) + eq(type(err), "string") + end) + + it("sets default buf options emulating scratch-buffer", function() + local tree = Tree({ bufnr = bufnr }) + + h.assert_buf_options(tree.bufnr, { + bufhidden = "hide", + buflisted = false, + buftype = "nofile", + swapfile = false, + }) + end) + + describe("(#deprecated) o.win_options", function() + it("sets default values for handling folds", function() + local tree = Tree({ winid = winid }) + + h.assert_win_options(tree.winid, { + foldmethod = "manual", + foldcolumn = "0", + wrap = false, + }) + end) + + it("sets values", function() + local initial_statusline = vim.api.nvim_win_get_option(winid, "statusline") + + local statusline = "test: win_options " .. math.random() + local tree = Tree({ + winid = winid, + win_options = { + statusline = statusline, + }, + }) + + h.assert_win_options(tree.winid, { + statusline = statusline, + }) + + vim.api.nvim_win_set_option(tree.winid, "statusline", initial_statusline) + end) + + it("has no effect if o.bufnr is present", function() + local initial_statusline = vim.api.nvim_win_get_option(winid, "statusline") + + Tree({ + bufnr = bufnr, + win_options = { + statusline = "test: win_options" .. math.random(), + }, + }) + + h.assert_win_options(winid, { + statusline = initial_statusline, + }) + end) + end) + + it("sets t.ns_id if o.ns_id is string", function() + local ns = "NuiTreeTest" + local tree = Tree({ bufnr = bufnr, ns_id = ns }) + + local namespaces = vim.api.nvim_get_namespaces() + + eq(tree.ns_id, namespaces[ns]) + end) + + it("sets t.ns_id if o.ns_id is number", function() + local ns = "NuiTreeTest" + local ns_id = vim.api.nvim_create_namespace(ns) + local tree = Tree({ bufnr = bufnr, ns_id = ns_id }) + + eq(tree.ns_id, ns_id) + end) + + it("uses o.get_node_id if provided", function() + local node_d2 = Tree.Node({ key = "depth two" }) + local node_d1 = Tree.Node({ key = "depth one" }, { node_d2 }) + Tree({ + bufnr = bufnr, + nodes = { node_d1 }, + get_node_id = function(node) + return node.key + end, + }) + + eq(node_d1:get_id(), node_d1.key) + eq(node_d2:get_id(), node_d2.key) + end) + + describe("default get_node_id", function() + it("returns id using n.id", function() + local node = Tree.Node({ id = "id", text = "text" }) + Tree({ bufnr = bufnr, nodes = { node } }) + + eq(node:get_id(), "-id") + end) + + it("returns id using parent_id + depth + n.text", function() + local node_d2 = Tree.Node({ text = { "depth two a", Text("depth two b") } }) + local node_d1 = Tree.Node({ text = "depth one" }, { node_d2 }) + Tree({ bufnr = bufnr, nodes = { node_d1 } }) + + eq(node_d1:get_id(), string.format("-%s-%s", node_d1:get_depth(), node_d1.text)) + eq( + node_d2:get_id(), + string.format( + "%s-%s-%s", + node_d2:get_parent_id(), + node_d2:get_depth(), + table.concat({ node_d2.text[1], node_d2.text[2]:content() }, "-") + ) + ) + end) + + it("returns id using random number", function() + math.randomseed(0) + local expected_id = "-" .. math.random() + math.randomseed(0) + + local node = Tree.Node({}) + Tree({ bufnr = bufnr, nodes = { node } }) + + eq(node:get_id(), expected_id) + end) + end) + + it("uses o.prepare_node if provided", function() + local function prepare_node(node, parent_node) + if not parent_node then + return node.text + end + + return parent_node.text .. ":" .. node.text + end + + local nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }, { + Tree.Node({ text = "b-1" }), + Tree.Node({ text = "b-2" }), + }), + Tree.Node({ text = "c" }), + } + + nodes[2]:expand() + + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + prepare_node = prepare_node, + }) + + tree:render() + + h.assert_buf_lines(tree.bufnr, { + "a", + "b", + "b:b-1", + "b:b-2", + "c", + }) + end) + + describe("default prepare_node", function() + it("throws if missing n.text", function() + local nodes = { + Tree.Node({ txt = "a" }), + Tree.Node({ txt = "b" }), + Tree.Node({ txt = "c" }), + } + + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + }) + + local ok, err = pcall(tree.render, tree) + eq(ok, false) + eq(type(err), "string") + end) + + it("uses n.text", function() + local nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = { "b-1", "b-2" } }), + Tree.Node({ text = "c" }), + } + + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + }) + + tree:render() + + h.assert_buf_lines(tree.bufnr, { + " a", + " b-1", + " b-2", + " c", + }) + end) + + it("renders arrow if children are present", function() + local nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }, { + Tree.Node({ text = "b-1" }), + Tree.Node({ text = { "b-2", "b-3" } }), + }), + Tree.Node({ text = "c" }), + } + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + }) + + tree:render() + + h.assert_buf_lines(tree.bufnr, { + " a", + " b", + " c", + }) + + nodes[2]:expand() + tree:render() + + h.assert_buf_lines(tree.bufnr, { + " a", + " b", + " b-1", + " b-2", + " b-3", + " c", + }) + end) + end) + + describe("method :get_node", function() + it("can get node under cursor", function() + local nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }), + Tree.Node({ text = "c" }), + } + + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + }) + + tree:render() + + local linenr = 3 + + vim.api.nvim_win_set_cursor(winid, { linenr, 0 }) + + eq({ tree:get_node() }, { nodes[3], linenr, linenr }) + end) + + it("can get node with id", function() + local b_node_children = { + Tree.Node({ text = "b-1" }), + Tree.Node({ text = { "b-2", "b-3" } }), + } + + local nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }, b_node_children), + Tree.Node({ text = "c" }), + } + + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + get_node_id = function(node) + return type(node.text) == "table" and table.concat(node.text, "-") or node.text + end, + }) + + tree:render() + + eq({ tree:get_node("b") }, { nodes[2], 2, 2 }) + + tree:get_node("b"):expand() + tree:render() + + eq({ tree:get_node("b-2-b-3") }, { b_node_children[2], 4, 5 }) + end) + + it("can get node on linenr", function() + local b_node_children = { + Tree.Node({ id = "b-1-b-2", text = { "b-1", "b-2" } }), + } + + local nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }, b_node_children), + Tree.Node({ text = "c" }), + } + + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + }) + + tree:render() + + eq({ tree:get_node(1) }, { nodes[1], 1, 1 }) + + tree:get_node(2):expand() + tree:render() + + eq({ tree:get_node(3) }, { b_node_children[1], 3, 4 }) + eq({ tree:get_node(4) }, { b_node_children[1], 3, 4 }) + end) + end) + + describe("method :get_nodes", function() + it("can get nodes at root", function() + local nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }, { + Tree.Node({ text = "b-1" }), + }), + } + + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + get_node_id = function(node) + return node.text + end, + }) + + eq(tree:get_nodes(), nodes) + end) + + it("can get nodes under parent node", function() + local child_nodes = { + Tree.Node({ text = "b-1" }), + } + + local tree = Tree({ + bufnr = bufnr, + nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }, child_nodes), + }, + get_node_id = function(node) + return node.text + end, + }) + + eq(tree:get_nodes("b"), child_nodes) + end) + end) + + describe("method :add_node", function() + it("throw if invalid parent_id", function() + local tree = Tree({ + bufnr = bufnr, + nodes = { + Tree.Node({ text = "x" }), + }, + }) + + local ok, err = pcall(tree.add_node, tree, Tree.Node({ text = "y" }), "invalid_parent_id") + eq(ok, false) + eq(type(err), "string") + end) + + it("can add node at root", function() + local tree = Tree({ + bufnr = bufnr, + nodes = { + Tree.Node({ text = "x" }), + }, + }) + + tree:add_node(Tree.Node({ text = "y" })) + + tree:render() + + h.assert_buf_lines(tree.bufnr, { + " x", + " y", + }) + + tree:add_node(Tree.Node({ text = "z" })) + + tree:render() + + h.assert_buf_lines(tree.bufnr, { + " x", + " y", + " z", + }) + end) + + it("can add node under parent node", function() + local nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }, { + Tree.Node({ text = "b-1" }), + }), + Tree.Node({ text = "c" }), + } + + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + get_node_id = function(node) + return node.text + end, + }) + + tree:add_node(Tree.Node({ text = "b-2" }), "b") + + tree:get_node("b"):expand() + + tree:add_node(Tree.Node({ text = "c-1" }), "c") + + tree:get_node("c"):expand() + + tree:render() + + h.assert_buf_lines(tree.bufnr, { + " a", + " b", + " b-1", + " b-2", + " c", + " c-1", + }) + end) + end) + + describe("method :set_nodes", function() + it("throw if invalid parent_id", function() + local tree = Tree({ + bufnr = bufnr, + nodes = { + Tree.Node({ text = "x" }), + }, + }) + + local ok, err = pcall(tree.set_nodes, tree, {}, "invalid_parent_id") + eq(ok, false) + eq(type(err), "string") + end) + + it("can set nodes at root", function() + local tree = Tree({ + bufnr = bufnr, + nodes = { + Tree.Node({ text = "x" }), + }, + }) + + tree:set_nodes({ + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }), + }) + + tree:render() + + h.assert_buf_lines(tree.bufnr, { + " a", + " b", + }) + + tree:set_nodes({ + Tree.Node({ text = "c" }), + }) + + tree:render() + + h.assert_buf_lines(tree.bufnr, { + " c", + }) + end) + + it("can set nodes under parent node", function() + local nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }, { + Tree.Node({ text = "b-1" }), + }), + Tree.Node({ text = "c" }), + } + + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + get_node_id = function(node) + return node.text + end, + }) + + tree:set_nodes({ + Tree.Node({ text = "b-2" }), + }, "b") + + tree:get_node("b"):expand() + + tree:set_nodes({ + Tree.Node({ text = "c-1" }), + Tree.Node({ text = "c-2" }), + }, "c") + + tree:get_node("c"):expand() + + tree:render() + + h.assert_buf_lines(tree.bufnr, { + " a", + " b", + " b-2", + " c", + " c-1", + " c-2", + }) + end) + end) + + describe("method :remove_node", function() + it("can remove node w/o parent", function() + local nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }, { + Tree.Node({ text = "b-1" }), + }), + Tree.Node({ text = "c" }), + } + + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + get_node_id = function(node) + return node.text + end, + }) + + tree:remove_node("a") + + tree:get_node("b"):expand() + + tree:render() + + eq( + vim.tbl_map(function(node) + return node:get_id() + end, tree:get_nodes()), + { "b", "c" } + ) + + h.assert_buf_lines(tree.bufnr, { + " b", + " b-1", + " c", + }) + end) + + it("can remove node w/ parent", function() + local nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }, { + Tree.Node({ text = "b-1" }), + }), + Tree.Node({ text = "c" }), + } + + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + get_node_id = function(node) + return node.text + end, + }) + + tree:remove_node("b-1") + + tree:render() + + eq(tree:get_node("b"):get_child_ids(), {}) + + h.assert_buf_lines(tree.bufnr, { + " a", + " b", + " c", + }) + end) + + it("removes children nodes recursively", function() + local nodes = { + Tree.Node({ text = "a" }, { + Tree.Node({ text = "a-1" }, { + Tree.Node({ text = "a-1-x" }), + }), + }), + } + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + get_node_id = function(node) + return node.text + end, + }) + h.neq(tree:get_node("a"), nil) + h.neq(tree:get_node("a-1"), nil) + h.neq(tree:get_node("a-1-x"), nil) + + tree:remove_node("a") + + eq(tree:get_node("a"), nil) + eq(tree:get_node("a-1"), nil) + eq(tree:get_node("a-1-x"), nil) + end) + end) + + describe("method :render", function() + it("handles unexpected case of missing node", function() + local nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }), + Tree.Node({ text = "c" }), + } + + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + get_node_id = function(node) + return node.text + end, + }) + + -- this should not happen normally + tree.nodes.by_id["a"] = nil + + tree:render() + + h.assert_buf_lines(tree.bufnr, { + " b", + " c", + }) + end) + + it("skips node if o.prepare_node returns nil", function() + local nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }), + Tree.Node({ text = "c" }), + } + + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + get_node_id = function(node) + return node.text + end, + prepare_node = function(node) + if node:get_id() == "b" then + return nil + end + + return node.text + end, + }) + + tree:render() + + h.assert_buf_lines(tree.bufnr, { + "a", + "c", + }) + end) + + it("supports param linenr_start", function() + local b_node_children = { + Tree.Node({ text = "b-1" }), + Tree.Node({ text = "b-2" }), + } + local nodes = { + Tree.Node({ text = "a" }), + Tree.Node({ text = "b" }, b_node_children), + } + + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + "NuiTreeTest", + "", + "NuiTreeTest", + }) + + local tree = Tree({ + bufnr = bufnr, + nodes = nodes, + get_node_id = function(node) + return node.text + end, + }) + + tree:render(2) + + h.assert_buf_lines(tree.bufnr, { + "NuiTreeTest", + " a", + " b", + "NuiTreeTest", + }) + + nodes[2]:expand() + + tree:render() + + h.assert_buf_lines(tree.bufnr, { + "NuiTreeTest", + " a", + " b", + " b-1", + " b-2", + "NuiTreeTest", + }) + + nodes[2]:collapse() + + tree:render(3) + + h.assert_buf_lines(tree.bufnr, { + "NuiTreeTest", + "", + " a", + " b", + "NuiTreeTest", + }) + end) + end) +end) + +describe("nui.tree.Node", function() + describe("method :has_children", function() + it("works before initialization", function() + local node_wo_children = Tree.Node({ text = "a" }) + local node_w_children = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) }) + + eq(node_wo_children._initialized, false) + eq(node_wo_children:has_children(), false) + + eq(node_w_children._initialized, false) + eq(type(node_w_children.__children), "table") + eq(node_w_children:has_children(), true) + end) + + it("works after initialization", function() + local node_wo_children = Tree.Node({ text = "a" }) + local node_w_children = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) }) + + Tree({ + bufnr = vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()), + nodes = { node_wo_children, node_w_children }, + }) + + eq(node_wo_children._initialized, true) + eq(node_wo_children:has_children(), false) + + eq(node_w_children._initialized, true) + eq(type(node_w_children.__children), "nil") + eq(node_w_children:has_children(), true) + end) + end) + + describe("method :expand", function() + it("returns true if not already expanded", function() + local node = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) }) + eq(node:is_expanded(), false) + eq(node:expand(), true) + eq(node:is_expanded(), true) + end) + + it("returns false if already expanded", function() + local node = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) }) + node:expand() + eq(node:is_expanded(), true) + eq(node:expand(), false) + eq(node:is_expanded(), true) + end) + + it("does work w/ zero child", function() + local node = Tree.Node({ text = "a" }, {}) + eq(node:is_expanded(), false) + eq(node:expand(), true) + eq(node:is_expanded(), true) + end) + + it("does not work w/o children", function() + local node = Tree.Node({ text = "a" }) + eq(node:is_expanded(), false) + eq(node:expand(), false) + eq(node:is_expanded(), false) + end) + end) + + describe("method :collapse", function() + it("returns true if not already collapsed", function() + local node = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) }) + node:expand() + eq(node:is_expanded(), true) + eq(node:collapse(), true) + eq(node:is_expanded(), false) + end) + + it("returns false if already collapsed", function() + local node = Tree.Node({ text = "b" }, { Tree.Node({ text = "b-1" }) }) + eq(node:is_expanded(), false) + eq(node:collapse(), false) + eq(node:is_expanded(), false) + end) + + it("does not work w/o children", function() + local node = Tree.Node({ text = "a" }) + eq(node:is_expanded(), false) + eq(node:collapse(), false) + eq(node:is_expanded(), false) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/.editorconfig b/.config/nvim/pack/tree/start/nvim-web-devicons/.editorconfig new file mode 100644 index 0000000..56b7562 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +insert_final_newline = true +end_of_line = lf + +[*.lua] +indent_style = space +indent_size = 2 + +[*.sh] +indent_style = space +indent_size = 2 + +[.luarc.json] +indent_style = tab diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/.gitattributes b/.config/nvim/pack/tree/start/nvim-web-devicons/.gitattributes new file mode 100644 index 0000000..66b816f --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/.gitattributes @@ -0,0 +1 @@ +lua/nvim-web-devicons/light/* linguist-generated=true diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/.github/ISSUE_TEMPLATE/bug_report.yml b/.config/nvim/pack/tree/start/nvim-web-devicons/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..9aaf055 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,60 @@ +name: Bug report +description: Report a problem +type: bug +body: + - type: markdown + attributes: + value: | + Before reporting: + * search [existing issues](https://github.com/nvim-tree/nvim-web-devicons/issues) + * ensure that nvim-web-devicons is updated to the latest version + * please ensure that you are running the latest version of a [Nerd Font](https://www.nerdfonts.com/) + * windows users please see [Windows and WSL not rendering icons properly on some terminals](https://github.com/nvim-tree/nvim-web-devicons?tab=readme-ov-file#windows-and-wsl-not-rendering-icons-properly-on-some-terminals) + + Please submit a Pull Request to add an icon, see [CONTRIBUTING.md](https://github.com/nvim-tree/nvim-web-devicons/blob/master/CONTRIBUTING.md) + - type: textarea + attributes: + label: "Description" + description: "A short description of the problem you are reporting." + validations: + required: true + - type: textarea + attributes: + label: "Neovim version" + description: "Output of `nvim --version`" + placeholder: | + NVIM v0.10.3 + Build type: RelWithDebInfo + LuaJIT 2.1.1731601260 + render: text + validations: + required: true + - type: input + attributes: + label: "nvim-web-devicons version" + description: "`cd /nvim-web-devicons.lua ; git log --format='%h' -n 1`" + placeholder: | + commit hash + validations: + required: true + - type: input + attributes: + label: "Operating system and version" + placeholder: "Linux 5.16.11-arch1-1, macOS 11.5, Windows 10" + validations: + required: true + - type: input + attributes: + label: "Windows variant" + placeholder: "WSL, PowerShell, cygwin, msys" + validations: + required: false + - type: textarea + attributes: + label: "Expected behavior" + description: "A description of the behavior you expected:" + - type: textarea + attributes: + label: "Actual behavior" + description: "Observed behavior (may optionally include images, videos or a screencast)." + diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/.github/ISSUE_TEMPLATE/feature_request.md b/.config/nvim/pack/tree/start/nvim-web-devicons/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..c8bd1da --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for nvim-web-devicons +title: '' +type: feature +assignees: '' + +--- +**Is this a request for a new icon?** +Please submit a Pull Request to add an icon, see [CONTRIBUTING.md](https://github.com/nvim-tree/nvim-web-devicons/blob/master/CONTRIBUTING.md) + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. + +**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. diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/.github/workflows/ci.yml b/.config/nvim/pack/tree/start/nvim-web-devicons/.github/workflows/ci.yml new file mode 100644 index 0000000..20f1adb --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +name: CI + +on: + pull_request: + branches: + - '*' + push: + branches: + - master + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: leafo/gh-actions-lua@v10 + with: + luaVersion: "5.1" + + - uses: leafo/gh-actions-luarocks@v4 + + - name: luacheck + run: | + luarocks install luacheck 1.1.1 + make lint + + style: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: stylua + uses: JohnnyMorganz/stylua-action@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + version: "v2.0.2" + args: --check lua scripts + + colors: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: rhysd/action-setup-vim@v1 + with: + neovim: true + + - name: make colors-check + run: make colors-check + + filetypes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: rhysd/action-setup-vim@v1 + with: + neovim: true + + - name: make filetypes + env: + VIMRUNTIME: /home/runner/nvim-stable/share/nvim/runtime + run: make filetypes diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/.github/workflows/pre-commit-autoupdate.yml b/.config/nvim/pack/tree/start/nvim-web-devicons/.github/workflows/pre-commit-autoupdate.yml new file mode 100644 index 0000000..6af2644 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/.github/workflows/pre-commit-autoupdate.yml @@ -0,0 +1,23 @@ +name: Pre-commit autoupdate +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: +permissions: + contents: write + pull-requests: write +jobs: + auto-update: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - run: pip install pre-commit + - run: pre-commit autoupdate + - uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: update/pre-commit-hooks + title: "chore: update pre-commit hooks" + commit-message: "chore: update pre-commit hooks" + body: Update versions of pre-commit hooks to latest version. diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/.github/workflows/release.yml b/.config/nvim/pack/tree/start/nvim-web-devicons/.github/workflows/release.yml new file mode 100644 index 0000000..39bb179 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/.github/workflows/release.yml @@ -0,0 +1,28 @@ +name: Release + +on: + push: + tags: + - 'v*' + + workflow_dispatch: + +jobs: + luarocks-upload: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: LuaRocks Upload + uses: nvim-neorocks/luarocks-tag-release@v5 + env: + LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }} + with: + summary: Nerd Font icons for neovim + detailed_description: | + Coloured Nerd Font file icons for neovim. + + Dark and light background variants. + + https://www.nerdfonts.com/ + license: MIT + labels: neovim diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/.github/workflows/semantic-pr-subject.yml b/.config/nvim/pack/tree/start/nvim-web-devicons/.github/workflows/semantic-pr-subject.yml new file mode 100644 index 0000000..5b9554f --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/.github/workflows/semantic-pr-subject.yml @@ -0,0 +1,15 @@ +name: Semantic Pull Request Subject +on: + pull_request: + types: + - opened + - reopened + - edited + - synchronize +jobs: + semantic-pr-subject: + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@v4.5.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/.gitignore b/.config/nvim/pack/tree/start/nvim-web-devicons/.gitignore new file mode 100644 index 0000000..5d89b4f --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/.gitignore @@ -0,0 +1,4 @@ +.lua +.luarocks +/vim-colortemplate/ +mini-align/ diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/.luacheckrc b/.config/nvim/pack/tree/start/nvim-web-devicons/.luacheckrc new file mode 100644 index 0000000..d2e66da --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/.luacheckrc @@ -0,0 +1,13 @@ +max_line_length = 120 + +globals = { + "vim", + "jit", + "bit", +} + +read_globals = { + "MiniAlign", +} + +files["lua/nvim-web-devicons/*/icons_*.lua"].max_line_length = 200 diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/.luarc.json b/.config/nvim/pack/tree/start/nvim-web-devicons/.luarc.json new file mode 100644 index 0000000..8816911 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/.luarc.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + "runtime.version" : "Lua 5.1", + "diagnostics": { + "globals": [ + "vim", + "jit", + "bit", + "MiniAlign" + ] + } +} diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/.pre-commit-config.yaml b/.config/nvim/pack/tree/start/nvim-web-devicons/.pre-commit-config.yaml new file mode 100644 index 0000000..6e8d00d --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +repos: + - repo: https://github.com/johnnymorganz/stylua + rev: v2.1.0 + hooks: + - id: stylua-github + fail_fast: true + verbose: true + types: [lua] + - repo: local + hooks: + - id: luacheck + name: Luacheck + description: Lints Lua files using Luacheck. + entry: luacheck -- + language: system + fail_fast: true + verbose: true + types: [lua] + - repo: local + hooks: + - id: colors + name: colors + description: Ensures Light Color Scheme version has been generated. + entry: make colors-check + language: system + require_serial: true + pass_filenames: false + verbose: true + diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/.stylua.toml b/.config/nvim/pack/tree/start/nvim-web-devicons/.stylua.toml new file mode 100644 index 0000000..ecb6dca --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/.stylua.toml @@ -0,0 +1,6 @@ +column_width = 120 +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 2 +quote_style = "AutoPreferDouble" +call_parentheses = "None" diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/.styluaignore b/.config/nvim/pack/tree/start/nvim-web-devicons/.styluaignore new file mode 100644 index 0000000..c8a610f --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/.styluaignore @@ -0,0 +1 @@ +lua/nvim-web-devicons/*/icons_*.lua diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/CONTRIBUTING.md b/.config/nvim/pack/tree/start/nvim-web-devicons/CONTRIBUTING.md new file mode 100644 index 0000000..f00155e --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/CONTRIBUTING.md @@ -0,0 +1,143 @@ +# Contributing to `nvim-web-devicons` + +Thank you for your contribution! + +## Prerequisites + +Code is formatted using *stylua* and linted using *luacheck*. + +You can install these with: + +```sh +cargo install stylua +luarocks install luacheck +``` + +or via your OS package manager e.g. *Arch Linux*: + +```sh +pacman -S stylua luacheck +``` + +## Adding icons + +Add or update icons in `lua/nvim-web-devicons/default/` directory + +There are five files where icons can be added: + +1. `icons_by_filename.lua` +2. `icons_by_file_extension.lua` +3. `icons_by_operating_system.lua` +4. `icons_by_desktop_environment.lua` +5. `icons_by_window_manager.lua` + +Add the icon to table in file **1.** if the icon is for a file that is always named that way, for example `.gitconfig`. +Add the icon to table in file **2.** if the icon is for all files with an extension, for example `vim`. +Add the icon to table in files **3.**, **4.** and **5.** if the icon is from an OS, DE or WM. + +Each icon must have the following structure (this is an example): + +```lua +[".gitconfig"] = { icon = "", color = "#41535b", cterm_color = "0", name = "GitConfig" }, +``` + +> [!IMPORTANT] +> Make sure each icon association occupies exactly one line. + +***All of the following keys are required:*** + +- `icon` glyph +- `color` must contain a color in the html notation +- `cterm_color` must contain a number (any number) + - the correct value for `cterm_color` will be generated by the script in next step +- `name` must only contain alphanumeric characters (don't use `/`, `-`, `_`) + +## Building + +Note: Ensure your current working directory is the repo root. + +Following your changes, please run: + +```sh +make +``` + +This will: + +1. Install required plugins: [vim-colortemplate](https://github.com/lifepillar/vim-colortemplate.git) and [mini.align](https://github.com/echasnovski/mini.align) if necessary +2. Generate cterm colors +3. Generate light color variants +4. Check style +5. Lint +6. Find missing filetypes + +You can automatically fix any style issues via: + +```sh +make style-fix +``` + +Please commit all files from `lua/nvim-web-devicons/default/`, `lua/nvim-web-devicons/light/` end `lua/nvim-web-devicons/filetypes.lua` + +## Test + +Run `:NvimWebDeviconsHiTest` to view the icons and their highlighting. + +Start *Neovim* with `TERM=xterm-256color nvim ...` to test cterm. + +Check with `&background` `dark` and `light` + +## Documentation + +When modifying or adding API, please update [Usage](README.md#Usage) + +## Pull Request + +Please reference any issues in the description e.g. "resolves #1234", which will be closed upon merge. + +Please check "allow edits by maintainers" to allow *nvim-web-devicons* maintainers to make small changes +such as documentation tweaks. + +## Subject + +The merge commit message will be the subject of the PR. + +A [Conventional Commits] subject will be validated by the Semantic Pull Request Subject CI job. +Reference the issue to be used in the release notes e.g. + +```txt +feat: add gradle icons +fix: update rust icon +feat(#192): :NvimWebDeviconsHiTest +``` + +Available types: + +- feat: A new feature +- fix: A bug fix +- docs: Documentation only changes +- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) +- refactor: A code change that neither fixes a bug nor adds a feature +- perf: A code change that improves performance +- test: Adding missing tests or correcting existing tests +- build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) +- ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) +- chore: Other changes that don't modify src or test files +- revert: Reverts a previous commit + +If in doubt, look at previous commits. + +See also [The Conventional Commits ultimate cheatsheet](https://gist.github.com/gabrielecanepa/fa6cca1a8ae96f77896fe70ddee65527) + +## Browser Font + +It is useful to see the actual glyphs in the pull request. That can be done by setting the browser font to your nerd font. + +Using firefox: + +- Settings -> General +- Fonts -> Advanced +- Change Monospace to "Hack Nerd Font Mono" or similar +- Uncheck "Allow pages to choose their own fonts, instead of your selections above" + +[Conventional Commits]: diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/LICENSE b/.config/nvim/pack/tree/start/nvim-web-devicons/LICENSE new file mode 100644 index 0000000..df4391d --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 nvim-tree + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/Makefile b/.config/nvim/pack/tree/start/nvim-web-devicons/Makefile new file mode 100644 index 0000000..f884256 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/Makefile @@ -0,0 +1,48 @@ +VIM_COLORTEMPLATE_VERSION = 2.2.3 +VIM_MINI_ALIGN_VERSION = 0.14.0 + +all: generate style-check lint filetypes + +generate: vim-colortemplate mini-align + rm -f lua/nvim-web-devicons/light/icons_by_*.lua + cp lua/nvim-web-devicons/default/icons_by_*.lua lua/nvim-web-devicons/light/ + nvim \ + --clean \ + --headless \ + --cmd "set rtp^=vim-colortemplate" \ + --cmd "set rtp^=mini-align" \ + -c 'source scripts/generate.lua' \ + -c 'source scripts/align.lua' \ + -c 'source scripts/sort_filetypes.lua' \ + -c 'qall' + +colors-check: generate + git diff --exit-code lua/nvim-web-devicons/default/ + git diff --exit-code lua/nvim-web-devicons/light/ + git diff --exit-code lua/nvim-web-devicons/filetypes.lua + +vim-colortemplate: + mkdir -p vim-colortemplate + curl -L https://github.com/lifepillar/vim-colortemplate/archive/refs/tags/v$(VIM_COLORTEMPLATE_VERSION).tar.gz | tar zx --directory vim-colortemplate --strip-components=1 + +mini-align: + mkdir -p mini-align + curl -L https://github.com/echasnovski/mini.align/archive/refs/tags/v$(VIM_MINI_ALIGN_VERSION).tar.gz | tar zx --directory mini-align --strip-components=1 + +style-check: + stylua . --check + +style-fix: + stylua . + +lint: + luacheck lua scripts + +filetypes: + ./scripts/filetypes.sh + +clean: + rm -rf vim-colortemplate + rm -rf mini-align + +.PHONY: all clean generate colors-check style-check style-fix lint filetypes diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/README.md b/.config/nvim/pack/tree/start/nvim-web-devicons/README.md new file mode 100644 index 0000000..eb961b1 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/README.md @@ -0,0 +1,282 @@ +# Nvim-web-devicons + +Provides [Nerd Font](https://www.nerdfonts.com/) [^1] icons (glyphs) for use by *Neovim* plugins: + +- Icons by: + - Extension + - Full name +- Colours +- Light and dark variants +- API to modify/add icons + +A `lua` fork of [vim-devicons](https://github.com/ryanoasis/vim-devicons). + +> [!TIP] +> nvim-web-devicons adds new icons as they are introduced to Nerd Fonts. Please ensure that you are using the latest version of the font as newly introduced icons may display incorrectly or as an "unknown character". + +> [!IMPORTANT] +> Nerd fonts moved some symbols with version 3.0. Version 2.3 is meant for transition, +> supporting both version 2 and version 3 icons. +> Nvim-web-devicons requires version 2.3 or above to work properly. If you are unable to update +> please use your plugin manager to pin version of nvim-web-dev icons to `nerd-v2-compat` tag. + +[^1]: Not limited to Nerd Font icons: unicode and other fonts may be used. + +## Requirements + +- [neovim >=0.7.0](https://github.com/neovim/neovim/wiki/Installing-Neovim) +- [A Patched Nerd Font](https://www.nerdfonts.com/) + +## Installation + +```vim +Plug 'nvim-tree/nvim-web-devicons' +``` + +or with [packer.nvim](https://github.com/wbthomason/packer.nvim) + +```lua +use 'nvim-tree/nvim-web-devicons' +``` + +or with [lazy.nvim](https://github.com/folke/lazy.nvim) + +```lua +{ "nvim-tree/nvim-web-devicons", opts = {} }, +``` + +[![LuaRocks](https://img.shields.io/luarocks/v/nvim-tree/nvim-web-devicons?logo=lua&color=purple)](https://luarocks.org/modules/nvim-tree/nvim-web-devicons) + +## Additional Icons + +Additional icons may be added for icons that are present in Nerd Fonts: use the [Cheat Sheet](https://www.nerdfonts.com/cheat-sheet) search for available icons. + +PRs are always welcome! Please see [CONTRIBUTING](CONTRIBUTING.md) + +> [!IMPORTANT] +> If the icon you need is not available on Nerd Fonts you first need to make a +> PR to a project that feeds glyphs to Nerd Fonts. Probably the most adequate +> project to contribute is . +> Take into account that **months** can pass before the icon is available on the +> Nerd Fonts project, once there feel free to open a PR on this project, see +> [CONTRIBUTING](CONTRIBUTING.md) + +## Usage + +### Viewing + +Run `:NvimWebDeviconsHiTest` to see all icons and their highlighting. + +### Variants + +Light or dark color variants of the icons depend on `&background`. +The variant can also be set manually in `setup` with the `variant` option. + +The variant is updated: + +- on `OptionSet` event for `background`, or +- after explicitly calling `require("nvim-web-devicons").refresh()`. + +However, be advised that the plugin using nvim-web-devicons may have cached the icons. + +### Case Sensitivity + +Filename icons e.g. `"Dockerfile"` are case insensitively matched. + +Extension icons e.g. `"lua"` are case sensitive. + +### Setup + +This adds all the highlight groups for the devicons +i.e. it calls `vim.api.nvim_set_hl` for all icons +this might need to be re-called in a `Colorscheme` to re-apply cleared highlights +if the color scheme changes + +```lua +require'nvim-web-devicons'.setup { + -- your personal icons can go here (to override) + -- you can specify color or cterm_color instead of specifying both of them + -- DevIcon will be appended to `name` + override = { + zsh = { + icon = "", + color = "#428850", + cterm_color = "65", + name = "Zsh" + } + }; + -- globally enable different highlight colors per icon (default to true) + -- if set to false all icons will have the default icon's color + color_icons = true; + -- globally enable default icons (default to false) + -- will get overriden by `get_icons` option + default = true; + -- globally enable "strict" selection of icons - icon will be looked up in + -- different tables, first by filename, and if not found by extension; this + -- prevents cases when file doesn't have any extension but still gets some icon + -- because its name happened to match some extension (default to false) + strict = true; + -- set the light or dark variant manually, instead of relying on `background` + -- (default to nil) + variant = "light|dark"; + -- same as `override` but specifically for overrides by filename + -- takes effect when `strict` is true + override_by_filename = { + [".gitignore"] = { + icon = "", + color = "#f1502f", + name = "Gitignore" + } + }; + -- same as `override` but specifically for overrides by extension + -- takes effect when `strict` is true + override_by_extension = { + ["log"] = { + icon = "", + color = "#81e043", + name = "Log" + } + }; + -- same as `override` but specifically for operating system + -- takes effect when `strict` is true + override_by_operating_system = { + ["apple"] = { + icon = "", + color = "#A2AAAD", + cterm_color = "248", + name = "Apple", + }, + }; +} +``` + +### Get Icon + +Get the icon for a given file by passing in the `name`, the `extension` and an *optional* options `table`. +The name is passed in to check for an exact match e.g. `.bashrc` if there is no exact name match the extension +is used. Calls `.setup()` if it hasn't already ran. + +```lua +require'nvim-web-devicons'.get_icon(filename, extension, options) +``` + +The optional `options` argument can used to change how the plugin works the keys include +`default = ` and `strict = `. If the default key is set to true this +function will return a default if there is no matching icon. If the strict key is set +to true this function will lookup icon specifically by filename, and if not found then +specifically by extension, and fallback to default icon if default key is set to true. +e.g. + +```lua +require'nvim-web-devicons'.get_icon(filename, extension, { default = true }) +``` + +You can check if the setup function was already called with: + +```lua +require'nvim-web-devicons'.has_loaded() +``` + +### Get icon and color code + +`get_icon_color` differs from `get_icon` only in the second return value. +`get_icon_cterm_color` returns cterm color instead of gui color +`get_icon` returns icon and highlight name. +If you want to get color code, you can use this function. + +```lua +local icon, color = require'nvim-web-devicons'.get_icon_color("init.lua", "lua") +assert(icon == "") +assert(color == "#51a0cf") +``` + +### Get all icons + +It is possible to get all of the registered icons with the `get_icons()` function: + +```lua +require'nvim-web-devicons'.get_icons() +``` + +This can be useful for debugging purposes or for creating custom highlights for each icon. + +Mapped categories can be fetched via: + +```lua +require'nvim-web-devicons'.get_icons_by_filename() +require'nvim-web-devicons'.get_icons_by_extension() +require'nvim-web-devicons'.get_icons_by_operating_system() +require'nvim-web-devicons'.get_icons_by_desktop_environment() +require'nvim-web-devicons'.get_icons_by_window_manager() +``` + +### Set an icon + +You can override individual icons with the `set_icon({...})` function: + +```lua +require("nvim-web-devicons").set_icon { + zsh = { + icon = "", + color = "#428850", + cterm_color = "65", + name = "Zsh" + } +} +``` + +You can override the default icon with the `set_default_icon(icon, color, cterm_color)` function: + +```lua +require("nvim-web-devicons").set_default_icon('', '#6d8086', 65) +``` + +### Getting and setting icons by filetype + +You can get the icon and colors associated with a filetype using the `by_filetype` functions: + +```lua +require("nvim-web-devicons").get_icon_by_filetype(filetype, opts) +require("nvim-web-devicons").get_icon_colors_by_filetype(filetype, opts) +require("nvim-web-devicons").get_icon_color_by_filetype(filetype, opts) +require("nvim-web-devicons").get_icon_cterm_color_by_filetype(filetype, opts) +``` + +or set the icon to use for a filetype with: + +```lua +require("nvim-web-devicons").set_icon_by_filetype { cpp = "c", pandoc = "md", } +``` + +These functions are the same as their counterparts without the `_by_filetype` suffix, +but they take a filetype instead of a name/extension. + +You can also use `get_icon_name_by_filetype(filetype)` to get the icon name associated with the filetype. + +## Known Issues + +### My `setup` Overrides Are Not Applied + +*Cause:* A plugin may be calling nvim-web-devicons `setup` before you do. Your `setup` call will be ignored. + +*Workaround:* Call nvim-web-devicons `setup` before the plugin's own `setup`. + +### Windows and WSL not rendering icons properly on some terminals + +On Windows and WSL, it is possible that the icons are not rendered properly when +using a terminal that relies on Windows' default system libraries. An example +of this is Alacritty ([#271](https://github.com/nvim-tree/nvim-web-devicons/issues/271#issuecomment-2081280928)). +Other terminals (e.g. Windows Terminal, and WezTerm) do no have this issue, as +they ship newer versions of these libraries. More precisely, they use newer +versions of `conpty.dll` and `OpenConsole.exe`. So, as a workaround to the +rendering issue, you need to make your terminal use these newer files. Whether +this is possible depends on the terminal you are using. Please refer to the +terminal's documentation for this. + +In the specific case of Alacritty, you need to place up-to-date `conpty.dll` and +`OpenConsole.exe` files in your `PATH`. Microsoft does not provide these files +directly, but you can get them from other terminal emulators that ship them. + +## Contributing + +PRs are always welcome! Please see [CONTRIBUTING](CONTRIBUTING.md) diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons.lua new file mode 100644 index 0000000..43ef9bc --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons.lua @@ -0,0 +1,470 @@ +local M = {} + +---@alias iconName string Name of the icon + +---@class Icon +---@field icon string Nerd-font glyph +---@field color string Hex color code +---@field cterm_color string cterm color code +---@field name iconName + +-- NOTE: When adding new icons, remember to add an entry to the `filetypes` table, if applicable. +local icons, icons_by_filename, icons_by_file_extension, icons_by_operating_system +local icons_by_desktop_environment, icons_by_window_manager +local user_icons + +local filetypes = require "nvim-web-devicons.filetypes" + +---@type Icon +local default_icon = { + icon = "", + color = "#6d8086", + cterm_color = "66", + name = "Default", +} + +function M.get_icons() + return icons +end + +function M.get_icons_by_filename() + return icons_by_filename +end + +function M.get_icons_by_extension() + return icons_by_file_extension +end + +function M.get_icons_by_operating_system() + return icons_by_operating_system +end + +function M.get_icons_by_desktop_environment() + return icons_by_desktop_environment +end + +function M.get_icons_by_window_manager() + return icons_by_window_manager +end + +local global_opts = { + override = {}, + strict = false, + default = false, + color_icons = true, + variant = nil, +} + +---Change all keys in a table to lowercase +---Remove entry when lowercase entry already exists +---@param t table +local function lowercase_keys(t) + if not t then + return + end + + for k, v in pairs(t) do + if type(k) == "string" then + local lower_k = k:lower() + if lower_k ~= k then + if not t[lower_k] then + t[lower_k] = v + end + t[k] = nil + end + end + end +end + +-- Set the current icons tables, depending on variant option, then &background +local function refresh_icons() + local theme + if global_opts.variant == "light" then + theme = require "nvim-web-devicons.icons-light" + elseif global_opts.variant == "dark" then + theme = require "nvim-web-devicons.icons-default" + else + if vim.o.background == "light" then + theme = require "nvim-web-devicons.icons-light" + else + theme = require "nvim-web-devicons.icons-default" + end + end + + icons_by_filename = theme.icons_by_filename + icons_by_file_extension = theme.icons_by_file_extension + icons_by_operating_system = theme.icons_by_operating_system + icons_by_desktop_environment = theme.icons_by_desktop_environment + icons_by_window_manager = theme.icons_by_window_manager + + -- filename matches are case insensitive + lowercase_keys(icons_by_filename) + + icons = vim.tbl_extend( + "keep", + {}, + icons_by_filename, + icons_by_file_extension, + icons_by_operating_system, + icons_by_desktop_environment, + icons_by_window_manager + ) + icons = vim.tbl_extend("force", icons, global_opts.override) + icons[1] = default_icon +end + +local function get_highlight_name(data) + if not global_opts.color_icons then + data = default_icon + end + + return data.name and "DevIcon" .. data.name +end + +local nvim_set_hl = vim.api.nvim_set_hl +local function set_up_highlight(icon_data) + if not global_opts.color_icons then + icon_data = default_icon + end + + local hl_group = get_highlight_name(icon_data) + if hl_group and (icon_data.color or icon_data.cterm_color) then + nvim_set_hl(0, get_highlight_name(icon_data), { + fg = icon_data.color, + ctermfg = tonumber(icon_data.cterm_color), + }) + end +end + +local function highlight_exists(group) + if not group then + return + end + + if vim.fn.has "nvim-0.9" == 1 then + local hl = vim.api.nvim_get_hl(0, { name = group, link = false }) + return not vim.tbl_isempty(hl) + else + local ok, hl = pcall(vim.api.nvim_get_hl_by_name, group, true) ---@diagnostic disable-line: deprecated + return ok and not (hl or {})[true] + end +end + +function M.set_up_highlights(allow_override) + if not global_opts.color_icons then + set_up_highlight(default_icon) + return + end + + for _, icon_data in pairs(icons) do + local has_color = icon_data.color or icon_data.cterm_color + local name_valid = icon_data.name + local defined_before = highlight_exists(get_highlight_name(icon_data)) + if has_color and name_valid and (allow_override or not defined_before) then + set_up_highlight(icon_data) + end + end +end + +local function get_highlight_foreground(icon_data) + if not global_opts.color_icons then + icon_data = default_icon + end + + local higroup = get_highlight_name(icon_data) + + local fg + if vim.fn.has "nvim-0.9" == 1 then + fg = vim.api.nvim_get_hl(0, { name = higroup, link = false }).fg + else + fg = vim.api.nvim_get_hl_by_name(higroup, true).foreground ---@diagnostic disable-line: deprecated + end + + return string.format("#%06x", fg) +end + +local function get_highlight_ctermfg(icon_data) + if not global_opts.color_icons then + icon_data = default_icon + end + + local higroup = get_highlight_name(icon_data) + + if vim.fn.has "nvim-0.9" == 1 then + --- @type string + --- @diagnostic disable-next-line: undefined-field vim.api.keyset.hl_info specifies cterm, not ctermfg + return vim.api.nvim_get_hl(0, { name = higroup, link = false }).ctermfg + else + return vim.api.nvim_get_hl_by_name(higroup, false).foreground ---@diagnostic disable-line: deprecated + end +end + +local function apply_user_icons() + if type(user_icons) ~= "table" then + return + end + + if user_icons.override and user_icons.override.default_icon then + default_icon = user_icons.override.default_icon + end + + local user_filename_icons = user_icons.override_by_filename + local user_file_ext_icons = user_icons.override_by_extension + local user_operating_system_icons = user_icons.override_by_operating_system + local user_desktop_environment_icons = user_icons.override_by_desktop_environment + local user_window_manager_icons = user_icons.override_by_window_manager + + -- filename matches are case insensitive + lowercase_keys(icons_by_filename) + lowercase_keys(user_icons.override) + lowercase_keys(user_icons.override_by_filename) + + icons = vim.tbl_extend( + "force", + icons, + user_icons.override or {}, + user_filename_icons or {}, + user_file_ext_icons or {}, + user_operating_system_icons or {}, + user_desktop_environment_icons or {}, + user_window_manager_icons or {} + ) + global_opts.override = vim.tbl_extend( + "force", + global_opts.override, + user_icons.override or {}, + user_filename_icons or {}, + user_file_ext_icons or {}, + user_operating_system_icons or {}, + user_desktop_environment_icons or {}, + user_window_manager_icons or {} + ) + + if user_filename_icons then + icons_by_filename = vim.tbl_extend("force", icons_by_filename, user_filename_icons) + end + if user_file_ext_icons then + icons_by_file_extension = vim.tbl_extend("force", icons_by_file_extension, user_file_ext_icons) + end + if user_operating_system_icons then + icons_by_operating_system = vim.tbl_extend("force", icons_by_operating_system, user_operating_system_icons) + end + if user_desktop_environment_icons then + icons_by_desktop_environment = vim.tbl_extend("force", icons_by_desktop_environment, user_desktop_environment_icons) + end + if user_window_manager_icons then + icons_by_window_manager = vim.tbl_extend("force", icons_by_window_manager, user_window_manager_icons) + end + + icons[1] = default_icon +end + +local loaded = false + +function M.has_loaded() + return loaded +end + +local if_nil = vim.F.if_nil +function M.setup(opts) + if loaded then + return + end + + loaded = true + + user_icons = opts or {} + + if user_icons.default then + global_opts.default = true + end + + if user_icons.strict then + global_opts.strict = true + end + + global_opts.color_icons = if_nil(user_icons.color_icons, global_opts.color_icons) + + if user_icons.variant == "light" or user_icons.variant == "dark" then + global_opts.variant = user_icons.variant + + -- Reload the icons after setting variant option + refresh_icons() + end + + apply_user_icons() + + M.set_up_highlights() + + vim.api.nvim_create_autocmd("ColorScheme", { + desc = "Re-apply icon colors after changing colorschemes", + group = vim.api.nvim_create_augroup("NvimWebDevicons", { clear = true }), + callback = M.set_up_highlights, + }) + + -- highlight test command + vim.api.nvim_create_user_command("NvimWebDeviconsHiTest", function() + require "nvim-web-devicons.hi-test"( + default_icon, + global_opts.override, + icons_by_filename, + icons_by_file_extension, + icons_by_operating_system, + icons_by_desktop_environment, + icons_by_window_manager + ) + end, { + desc = "nvim-web-devicons: highlight test", + }) +end + +function M.get_default_icon() + return default_icon +end + +-- recursively iterate over each segment separated by '.' to parse extension with multiple dots in filename +local function iterate_multi_dotted_extension(name, icon_table) + if name == nil then + return nil + end + + local compound_ext = name:match "%.(.*)" + local icon = icon_table[compound_ext] + if icon then + return icon + end + + return iterate_multi_dotted_extension(compound_ext, icon_table) +end + +local function get_icon_by_extension(name, ext, opts) + local is_strict = if_nil(opts and opts.strict, global_opts.strict) + local icon_table = is_strict and icons_by_file_extension or icons + + if ext ~= nil then + return icon_table[ext] + end + + return iterate_multi_dotted_extension(name, icon_table) +end + +local function get_icon_data(name, ext, opts) + if type(name) == "string" then + name = name:lower() + end + + if not loaded then + M.setup() + end + + local has_default = if_nil(opts and opts.default, global_opts.default) + local is_strict = if_nil(opts and opts.strict, global_opts.strict) + local icon_data + if is_strict then + icon_data = icons_by_filename[name] or get_icon_by_extension(name, ext, opts) or (has_default and default_icon) + else + icon_data = icons[name] or get_icon_by_extension(name, ext, opts) or (has_default and default_icon) + end + + return icon_data +end + +function M.get_icon(name, ext, opts) + local icon_data = get_icon_data(name, ext, opts) + + if icon_data then + return icon_data.icon, get_highlight_name(icon_data) + end +end + +function M.get_icon_name_by_filetype(ft) + return filetypes[ft] +end + +function M.get_icon_by_filetype(ft, opts) + local name = M.get_icon_name_by_filetype(ft) + opts = opts or {} + opts.strict = false + return M.get_icon(name or "", nil, opts) +end + +function M.get_icon_colors(name, ext, opts) + local icon_data = get_icon_data(name, ext, opts) + + if icon_data then + local color = icon_data.color + local cterm_color = icon_data.cterm_color + if icon_data.name and highlight_exists(get_highlight_name(icon_data)) then + color = get_highlight_foreground(icon_data) or color + cterm_color = get_highlight_ctermfg(icon_data) or cterm_color + end + return icon_data.icon, color, cterm_color + end +end + +function M.get_icon_colors_by_filetype(ft, opts) + local name = M.get_icon_name_by_filetype(ft) + return M.get_icon_colors(name or "", nil, opts) +end + +function M.get_icon_color(name, ext, opts) + local data = { M.get_icon_colors(name, ext, opts) } + return data[1], data[2] +end + +function M.get_icon_color_by_filetype(ft, opts) + local name = M.get_icon_name_by_filetype(ft) + opts = opts or {} + opts.strict = false + return M.get_icon_color(name or "", nil, opts) +end + +function M.get_icon_cterm_color(name, ext, opts) + local data = { M.get_icon_colors(name, ext, opts) } + return data[1], data[3] +end + +function M.get_icon_cterm_color_by_filetype(ft, opts) + local name = M.get_icon_name_by_filetype(ft) + return M.get_icon_cterm_color(name or "", nil, opts) +end + +function M.set_icon(user_icons_opts) + icons = vim.tbl_extend("force", icons, user_icons_opts or {}) + global_opts.override = vim.tbl_extend("force", global_opts.override, user_icons_opts or {}) + if not global_opts.color_icons then + return + end + + for _, icon_data in pairs(user_icons_opts) do + set_up_highlight(icon_data) + end +end + +function M.set_icon_by_filetype(user_filetypes) + filetypes = vim.tbl_extend("force", filetypes, user_filetypes or {}) +end + +function M.set_default_icon(icon, color, cterm_color) + default_icon.icon = icon + default_icon.color = color + default_icon.cterm_color = cterm_color + set_up_highlight(default_icon) +end + +-- Load the icons already, the loaded tables depend on the 'background' setting. +refresh_icons() + +function M.refresh() + refresh_icons() + apply_user_icons() + M.set_up_highlights(true) +end + +-- Change icon set on background change +vim.api.nvim_create_autocmd("OptionSet", { + pattern = "background", + callback = M.refresh, +}) + +return M diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_desktop_environment.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_desktop_environment.lua new file mode 100644 index 0000000..24dcefc --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_desktop_environment.lua @@ -0,0 +1,10 @@ +return { + ["budgie"] = { icon = "", color = "#4E5361", cterm_color = "240", name = "Budgie" }, + ["cinnamon"] = { icon = "", color = "#DC682E", cterm_color = "166", name = "Cinnamon" }, + ["gnome"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "GNOME" }, + ["lxde"] = { icon = "", color = "#A4A4A4", cterm_color = "248", name = "LXDE" }, + ["lxqt"] = { icon = "", color = "#0191D2", cterm_color = "32", name = "LXQt" }, + ["mate"] = { icon = "", color = "#9BDA5C", cterm_color = "113", name = "MATE" }, + ["plasma"] = { icon = "", color = "#1B89F4", cterm_color = "33", name = "KDEPlasma" }, + ["xfce"] = { icon = "", color = "#00AADF", cterm_color = "74", name = "Xfce" }, +} --[[@as table]] diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_file_extension.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_file_extension.lua new file mode 100644 index 0000000..875b6b4 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_file_extension.lua @@ -0,0 +1,487 @@ +return { + + ["3gp"] = { icon = "", color = "#FD971F", cterm_color = "208", name = "3gp" }, + ["3mf"] = { icon = "󰆧", color = "#888888", cterm_color = "102", name = "3DObjectFile" }, + ["7z"] = { icon = "", color = "#ECA517", cterm_color = "214", name = "7z" }, + ["Dockerfile"] = { icon = "󰡨", color = "#458EE6", cterm_color = "68", name = "Dockerfile" }, + ["R"] = { icon = "󰟔", color = "#2266BA", cterm_color = "25", name = "R" }, + ["a"] = { icon = "", color = "#DCDDD6", cterm_color = "253", name = "StaticLibraryArchive" }, + ["aac"] = { icon = "", color = "#00AFFF", cterm_color = "39", name = "AdvancedAudioCoding" }, + ["ada"] = { icon = "", color = "#599EFF", cterm_color = "111", name = "AdaFile" }, + ["adb"] = { icon = "", color = "#599EFF", cterm_color = "111", name = "AdaBody" }, + ["ads"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "AdaSpecification" }, + ["ai"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "Ai" }, + ["aif"] = { icon = "", color = "#00AFFF", cterm_color = "39", name = "AudioInterchangeFileFormat" }, + ["aiff"] = { icon = "", color = "#00AFFF", cterm_color = "39", name = "AudioInterchangeFileFormat" }, + ["android"] = { icon = "", color = "#34A853", cterm_color = "35", name = "Android" }, + ["ape"] = { icon = "", color = "#00AFFF", cterm_color = "39", name = "MonkeysAudio" }, + ["apk"] = { icon = "", color = "#34A853", cterm_color = "35", name = "apk" }, + ["apl"] = { icon = "", color = "#24A148", cterm_color = "35", name = "APL" }, + ["app"] = { icon = "", color = "#9F0500", cterm_color = "124", name = "App" }, + ["applescript"] = { icon = "", color = "#6D8085", cterm_color = "66", name = "AppleScript" }, + ["asc"] = { icon = "󰦝", color = "#576D7F", cterm_color = "242", name = "Asc" }, + ["asm"] = { icon = "", color = "#0091BD", cterm_color = "31", name = "ASM" }, + ["ass"] = { icon = "󰨖", color = "#FFB713", cterm_color = "214", name = "Ass" }, + ["astro"] = { icon = "", color = "#E23F67", cterm_color = "197", name = "Astro" }, + ["avif"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Avif" }, + ["awk"] = { icon = "", color = "#4D5A5E", cterm_color = "240", name = "Awk" }, + ["azcli"] = { icon = "", color = "#0078D4", cterm_color = "32", name = "AzureCli" }, + ["bak"] = { icon = "󰁯", color = "#6D8086", cterm_color = "66", name = "Backup" }, + ["bash"] = { icon = "", color = "#89E051", cterm_color = "113", name = "Bash" }, + ["bat"] = { icon = "", color = "#C1F12E", cterm_color = "191", name = "Bat" }, + ["bazel"] = { icon = "", color = "#89E051", cterm_color = "113", name = "Bazel" }, + ["bib"] = { icon = "󱉟", color = "#CBCB41", cterm_color = "185", name = "BibTeX" }, + ["bicep"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Bicep" }, + ["bicepparam"] = { icon = "", color = "#9F74B3", cterm_color = "133", name = "BicepParameters" }, + ["bin"] = { icon = "", color = "#9F0500", cterm_color = "124", name = "Bin" }, + ["blade.php"] = { icon = "", color = "#F05340", cterm_color = "203", name = "Blade" }, + ["blend"] = { icon = "󰂫", color = "#EA7600", cterm_color = "208", name = "Blender" }, + ["blp"] = { icon = "󰺾", color = "#5796E2", cterm_color = "68", name = "Blueprint" }, + ["bmp"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Bmp" }, + ["bqn"] = { icon = "", color = "#24A148", cterm_color = "35", name = "APL" }, + ["brep"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "BoundaryRepresentation" }, + ["bz"] = { icon = "", color = "#ECA517", cterm_color = "214", name = "Bz" }, + ["bz2"] = { icon = "", color = "#ECA517", cterm_color = "214", name = "Bz2" }, + ["bz3"] = { icon = "", color = "#ECA517", cterm_color = "214", name = "Bz3" }, + ["bzl"] = { icon = "", color = "#89E051", cterm_color = "113", name = "Bzl" }, + ["c"] = { icon = "", color = "#599EFF", cterm_color = "111", name = "C" }, + ["c++"] = { icon = "", color = "#F34B7D", cterm_color = "204", name = "CPlusPlus" }, + ["cache"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "Cache" }, + ["cast"] = { icon = "", color = "#FD971F", cterm_color = "208", name = "Asciinema" }, + ["cbl"] = { icon = "", color = "#005CA5", cterm_color = "25", name = "Cobol" }, + ["cc"] = { icon = "", color = "#F34B7D", cterm_color = "204", name = "CPlusPlus" }, + ["ccm"] = { icon = "", color = "#F34B7D", cterm_color = "204", name = "CPlusPlusModule" }, + ["cfc"] = { icon = "", color = "#01A4BA", cterm_color = "38", name = "ColdFusionScript" }, + ["cfg"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "Configuration" }, + ["cfm"] = { icon = "", color = "#01A4BA", cterm_color = "38", name = "ColdFusionTag" }, + ["cjs"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "Cjs" }, + ["clj"] = { icon = "", color = "#8DC149", cterm_color = "113", name = "Clojure" }, + ["cljc"] = { icon = "", color = "#8DC149", cterm_color = "113", name = "ClojureC" }, + ["cljd"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "ClojureDart" }, + ["cljs"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "ClojureJS" }, + ["cmake"] = { icon = "", color = "#DCE3EB", cterm_color = "254", name = "CMake" }, + ["cob"] = { icon = "", color = "#005CA5", cterm_color = "25", name = "Cobol" }, + ["cobol"] = { icon = "", color = "#005CA5", cterm_color = "25", name = "Cobol" }, + ["coffee"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "Coffee" }, + ["conda"] = { icon = "", color = "#43B02A", cterm_color = "34", name = "Conda" }, + ["conf"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "Conf" }, + ["config.ru"] = { icon = "", color = "#701516", cterm_color = "52", name = "ConfigRu" }, + ["cow"] = { icon = "󰆚", color = "#965824", cterm_color = "130", name = "CowsayFile" }, + ["cp"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Cp" }, + ["cpp"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Cpp" }, + ["cppm"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Cppm" }, + ["cpy"] = { icon = "", color = "#005CA5", cterm_color = "25", name = "Cobol" }, + ["cr"] = { icon = "", color = "#C8C8C8", cterm_color = "251", name = "Crystal" }, + ["crdownload"] = { icon = "", color = "#44CDA8", cterm_color = "43", name = "Crdownload" }, + ["cs"] = { icon = "󰌛", color = "#596706", cterm_color = "58", name = "Cs" }, + ["csh"] = { icon = "", color = "#4D5A5E", cterm_color = "240", name = "Csh" }, + ["cshtml"] = { icon = "󱦗", color = "#512BD4", cterm_color = "56", name = "RazorPage" }, + ["cson"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "Cson" }, + ["csproj"] = { icon = "󰪮", color = "#512BD4", cterm_color = "56", name = "CSharpProject" }, + ["css"] = { icon = "", color = "#663399", cterm_color = "91", name = "Css" }, + ["csv"] = { icon = "", color = "#89E051", cterm_color = "113", name = "Csv" }, + ["cts"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Cts" }, + ["cu"] = { icon = "", color = "#89E051", cterm_color = "113", name = "cuda" }, + ["cue"] = { icon = "󰲹", color = "#ED95AE", cterm_color = "211", name = "Cue" }, + ["cuh"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "cudah" }, + ["cxx"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Cxx" }, + ["cxxm"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Cxxm" }, + ["d"] = { icon = "", color = "#B03931", cterm_color = "124", name = "D" }, + ["d.ts"] = { icon = "", color = "#D59855", cterm_color = "172", name = "TypeScriptDeclaration" }, + ["dart"] = { icon = "", color = "#03589C", cterm_color = "25", name = "Dart" }, + ["db"] = { icon = "", color = "#DAD8D8", cterm_color = "188", name = "Db" }, + ["dconf"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "Dconf" }, + ["desktop"] = { icon = "", color = "#563D7C", cterm_color = "54", name = "DesktopEntry" }, + ["diff"] = { icon = "", color = "#41535B", cterm_color = "239", name = "Diff" }, + ["dll"] = { icon = "", color = "#4D2C0B", cterm_color = "52", name = "Dll" }, + ["doc"] = { icon = "󰈬", color = "#185ABD", cterm_color = "26", name = "Doc" }, + ["dockerignore"] = { icon = "󰡨", color = "#458EE6", cterm_color = "68", name = "DockerIgnore" }, + ["docx"] = { icon = "󰈬", color = "#185ABD", cterm_color = "26", name = "Docx" }, + ["dot"] = { icon = "󱁉", color = "#30638E", cterm_color = "24", name = "Dot" }, + ["download"] = { icon = "", color = "#44CDA8", cterm_color = "43", name = "Download" }, + ["drl"] = { icon = "", color = "#FFAFAF", cterm_color = "217", name = "Drools" }, + ["dropbox"] = { icon = "", color = "#0061FE", cterm_color = "27", name = "Dropbox" }, + ["dump"] = { icon = "", color = "#DAD8D8", cterm_color = "188", name = "Dump" }, + ["dwg"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "AutoCADDwg" }, + ["dxf"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "AutoCADDxf" }, + ["ebook"] = { icon = "", color = "#EAB16D", cterm_color = "215", name = "Ebook" }, + ["ebuild"] = { icon = "", color = "#4C416E", cterm_color = "60", name = "GentooBuild" }, + ["edn"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Edn" }, + ["eex"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Eex" }, + ["ejs"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "Ejs" }, + ["el"] = { icon = "", color = "#8172BE", cterm_color = "97", name = "Elisp" }, + ["elc"] = { icon = "", color = "#8172BE", cterm_color = "97", name = "Elisp" }, + ["elf"] = { icon = "", color = "#9F0500", cterm_color = "124", name = "Elf" }, + ["elm"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Elm" }, + ["eln"] = { icon = "", color = "#8172BE", cterm_color = "97", name = "Elisp" }, + ["env"] = { icon = "", color = "#FAF743", cterm_color = "227", name = "Env" }, + ["eot"] = { icon = "", color = "#ECECEC", cterm_color = "255", name = "EmbeddedOpenTypeFont" }, + ["epp"] = { icon = "", color = "#FFA61A", cterm_color = "214", name = "Epp" }, + ["epub"] = { icon = "", color = "#EAB16D", cterm_color = "215", name = "Epub" }, + ["erb"] = { icon = "", color = "#701516", cterm_color = "52", name = "Erb" }, + ["erl"] = { icon = "", color = "#B83998", cterm_color = "163", name = "Erl" }, + ["ex"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Ex" }, + ["exe"] = { icon = "", color = "#9F0500", cterm_color = "124", name = "Exe" }, + ["exs"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Exs" }, + ["f#"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Fsharp" }, + ["f3d"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "Fusion360" }, + ["f90"] = { icon = "󱈚", color = "#734F96", cterm_color = "97", name = "Fortran" }, + ["fbx"] = { icon = "󰆧", color = "#888888", cterm_color = "102", name = "3DObjectFile" }, + ["fcbak"] = { icon = "", color = "#CB333B", cterm_color = "160", name = "FreeCAD" }, + ["fcmacro"] = { icon = "", color = "#CB333B", cterm_color = "160", name = "FreeCAD" }, + ["fcmat"] = { icon = "", color = "#CB333B", cterm_color = "160", name = "FreeCAD" }, + ["fcparam"] = { icon = "", color = "#CB333B", cterm_color = "160", name = "FreeCAD" }, + ["fcscript"] = { icon = "", color = "#CB333B", cterm_color = "160", name = "FreeCAD" }, + ["fcstd"] = { icon = "", color = "#CB333B", cterm_color = "160", name = "FreeCAD" }, + ["fcstd1"] = { icon = "", color = "#CB333B", cterm_color = "160", name = "FreeCAD" }, + ["fctb"] = { icon = "", color = "#CB333B", cterm_color = "160", name = "FreeCAD" }, + ["fctl"] = { icon = "", color = "#CB333B", cterm_color = "160", name = "FreeCAD" }, + ["fdmdownload"] = { icon = "", color = "#44CDA8", cterm_color = "43", name = "Fdmdownload" }, + ["feature"] = { icon = "", color = "#00A818", cterm_color = "34", name = "Feature" }, + ["fish"] = { icon = "", color = "#4D5A5E", cterm_color = "240", name = "Fish" }, + ["flac"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "FreeLosslessAudioCodec" }, + ["flc"] = { icon = "", color = "#ECECEC", cterm_color = "255", name = "FIGletFontControl" }, + ["flf"] = { icon = "", color = "#ECECEC", cterm_color = "255", name = "FIGletFontFormat" }, + ["fnl"] = { icon = "", color = "#FFF3D7", cterm_color = "230", name = "Fennel" }, + ["fodg"] = { icon = "", color = "#FFFB57", cterm_color = "227", name = "LibreOfficeGraphics" }, + ["fodp"] = { icon = "", color = "#FE9C45", cterm_color = "215", name = "LibreOfficeImpress" }, + ["fods"] = { icon = "", color = "#78FC4E", cterm_color = "119", name = "LibreOfficeCalc" }, + ["fodt"] = { icon = "", color = "#2DCBFD", cterm_color = "81", name = "LibreOfficeWriter" }, + ["frag"] = { icon = "", color = "#5586A6", cterm_color = "67", name = "FragmentShader" }, + ["fs"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Fs" }, + ["fsi"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Fsi" }, + ["fsscript"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Fsscript" }, + ["fsx"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Fsx" }, + ["gcode"] = { icon = "󰐫", color = "#1471AD", cterm_color = "32", name = "GCode" }, + ["gd"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "GDScript" }, + ["gemspec"] = { icon = "", color = "#701516", cterm_color = "52", name = "Gemspec" }, + ["geom"] = { icon = "", color = "#5586A6", cterm_color = "67", name = "GeometryShader" }, + ["gif"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Gif" }, + ["git"] = { icon = "", color = "#F14C28", cterm_color = "196", name = "GitLogo" }, + ["glb"] = { icon = "", color = "#FFB13B", cterm_color = "214", name = "BinaryGLTF" }, + ["gleam"] = { icon = "", color = "#FFAFF3", cterm_color = "219", name = "Gleam" }, + ["glsl"] = { icon = "", color = "#5586A6", cterm_color = "67", name = "OpenGLShadingLanguage" }, + ["gnumakefile"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "Makefile" }, + ["go"] = { icon = "", color = "#00ADD8", cterm_color = "38", name = "Go" }, + ["godot"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "GodotProject" }, + ["gpr"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "GPRBuildProject" }, + ["gql"] = { icon = "", color = "#E535AB", cterm_color = "199", name = "GraphQL" }, + ["gradle"] = { icon = "", color = "#005F87", cterm_color = "24", name = "Gradle" }, + ["graphql"] = { icon = "", color = "#E535AB", cterm_color = "199", name = "GraphQL" }, + ["gresource"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "GTK" }, + ["gv"] = { icon = "󱁉", color = "#30638E", cterm_color = "24", name = "Gv" }, + ["gz"] = { icon = "", color = "#ECA517", cterm_color = "214", name = "Gz" }, + ["h"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "H" }, + ["haml"] = { icon = "", color = "#EAEAE1", cterm_color = "255", name = "Haml" }, + ["hbs"] = { icon = "", color = "#F0772B", cterm_color = "202", name = "Hbs" }, + ["heex"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Heex" }, + ["hex"] = { icon = "", color = "#2E63FF", cterm_color = "27", name = "Hexadecimal" }, + ["hh"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Hh" }, + ["hpp"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Hpp" }, + ["hrl"] = { icon = "", color = "#B83998", cterm_color = "163", name = "Hrl" }, + ["hs"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Hs" }, + ["htm"] = { icon = "", color = "#E34C26", cterm_color = "196", name = "Htm" }, + ["html"] = { icon = "", color = "#E44D26", cterm_color = "196", name = "Html" }, + ["http"] = { icon = "", color = "#008EC7", cterm_color = "31", name = "HTTP" }, + ["huff"] = { icon = "󰡘", color = "#4242C7", cterm_color = "56", name = "Huff" }, + ["hurl"] = { icon = "", color = "#FF0288", cterm_color = "198", name = "Hurl" }, + ["hx"] = { icon = "", color = "#EA8220", cterm_color = "208", name = "Haxe" }, + ["hxx"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Hxx" }, + ["ical"] = { icon = "", color = "#2B2E83", cterm_color = "18", name = "Ical" }, + ["icalendar"] = { icon = "", color = "#2B2E83", cterm_color = "18", name = "Icalendar" }, + ["ico"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "Ico" }, + ["ics"] = { icon = "", color = "#2B2E83", cterm_color = "18", name = "Ics" }, + ["ifb"] = { icon = "", color = "#2B2E83", cterm_color = "18", name = "Ifb" }, + ["ifc"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "Ifc" }, + ["ige"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "Ige" }, + ["iges"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "Iges" }, + ["igs"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "Igs" }, + ["image"] = { icon = "", color = "#D0BEC8", cterm_color = "181", name = "Image" }, + ["img"] = { icon = "", color = "#D0BEC8", cterm_color = "181", name = "Img" }, + ["import"] = { icon = "", color = "#ECECEC", cterm_color = "255", name = "ImportConfiguration" }, + ["info"] = { icon = "", color = "#FFFFCD", cterm_color = "230", name = "Info" }, + ["ini"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "Ini" }, + ["ino"] = { icon = "", color = "#56B6C2", cterm_color = "73", name = "Arduino" }, + ["ipynb"] = { icon = "", color = "#F57D01", cterm_color = "208", name = "Notebook" }, + ["iso"] = { icon = "", color = "#D0BEC8", cterm_color = "181", name = "Iso" }, + ["ixx"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Ixx" }, + ["jar"] = { icon = "", color = "#ffaf67", cterm_color = "215", name = "Jar" }, + ["java"] = { icon = "", color = "#CC3E44", cterm_color = "167", name = "Java" }, + ["jl"] = { icon = "", color = "#A270BA", cterm_color = "133", name = "Jl" }, + ["jpeg"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Jpeg" }, + ["jpg"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Jpg" }, + ["js"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "Js" }, + ["json"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "Json" }, + ["json5"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "Json5" }, + ["jsonc"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "Jsonc" }, + ["jsx"] = { icon = "", color = "#20C2E3", cterm_color = "45", name = "Jsx" }, + ["jwmrc"] = { icon = "", color = "#0078CD", cterm_color = "32", name = "JWM" }, + ["jxl"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "JpegXl" }, + ["kbx"] = { icon = "󰯄", color = "#737672", cterm_color = "243", name = "Kbx" }, + ["kdb"] = { icon = "", color = "#529B34", cterm_color = "71", name = "Kdb" }, + ["kdbx"] = { icon = "", color = "#529B34", cterm_color = "71", name = "Kdbx" }, + ["kdenlive"] = { icon = "", color = "#83B8F2", cterm_color = "110", name = "Kdenlive" }, + ["kdenlivetitle"] = { icon = "", color = "#83B8F2", cterm_color = "110", name = "Kdenlive" }, + ["kicad_dru"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "KiCad" }, + ["kicad_mod"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "KiCad" }, + ["kicad_pcb"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "KiCad" }, + ["kicad_prl"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "KiCad" }, + ["kicad_pro"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "KiCad" }, + ["kicad_sch"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "KiCad" }, + ["kicad_sym"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "KiCad" }, + ["kicad_wks"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "KiCad" }, + ["ko"] = { icon = "", color = "#DCDDD6", cterm_color = "253", name = "LinuxKernelObject" }, + ["kpp"] = { icon = "", color = "#F245FB", cterm_color = "201", name = "Krita" }, + ["kra"] = { icon = "", color = "#F245FB", cterm_color = "201", name = "Krita" }, + ["krz"] = { icon = "", color = "#F245FB", cterm_color = "201", name = "Krita" }, + ["ksh"] = { icon = "", color = "#4D5A5E", cterm_color = "240", name = "Ksh" }, + ["kt"] = { icon = "", color = "#7F52FF", cterm_color = "99", name = "Kotlin" }, + ["kts"] = { icon = "", color = "#7F52FF", cterm_color = "99", name = "KotlinScript" }, + ["lck"] = { icon = "", color = "#BBBBBB", cterm_color = "250", name = "Lock" }, + ["leex"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Leex" }, + ["less"] = { icon = "", color = "#563D7C", cterm_color = "54", name = "Less" }, + ["lff"] = { icon = "", color = "#ECECEC", cterm_color = "255", name = "LibrecadFontFile" }, + ["lhs"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Lhs" }, + ["lib"] = { icon = "", color = "#4D2C0B", cterm_color = "52", name = "Lib" }, + ["license"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "License" }, + ["liquid"] = { icon = "", color = "#95BF47", cterm_color = "106", name = "Liquid" }, + ["lock"] = { icon = "", color = "#BBBBBB", cterm_color = "250", name = "Lock" }, + ["log"] = { icon = "󰌱", color = "#DDDDDD", cterm_color = "253", name = "Log" }, + ["lrc"] = { icon = "󰨖", color = "#FFB713", cterm_color = "214", name = "Lrc" }, + ["lua"] = { icon = "", color = "#51A0CF", cterm_color = "74", name = "Lua" }, + ["luac"] = { icon = "", color = "#51A0CF", cterm_color = "74", name = "Lua" }, + ["luau"] = { icon = "", color = "#00A2FF", cterm_color = "75", name = "Luau" }, + ["m"] = { icon = "", color = "#599EFF", cterm_color = "111", name = "ObjectiveC" }, + ["m3u"] = { icon = "󰲹", color = "#ED95AE", cterm_color = "211", name = "M3u" }, + ["m3u8"] = { icon = "󰲹", color = "#ED95AE", cterm_color = "211", name = "M3u8" }, + ["m4a"] = { icon = "", color = "#00AFFF", cterm_color = "39", name = "MPEG4" }, + ["m4v"] = { icon = "", color = "#FD971F", cterm_color = "208", name = "M4V" }, + ["magnet"] = { icon = "", color = "#A51B16", cterm_color = "124", name = "Magnet" }, + ["makefile"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "Makefile" }, + ["markdown"] = { icon = "", color = "#DDDDDD", cterm_color = "253", name = "Markdown" }, + ["material"] = { icon = "", color = "#B83998", cterm_color = "163", name = "Material" }, + ["md"] = { icon = "", color = "#DDDDDD", cterm_color = "253", name = "Md" }, + ["md5"] = { icon = "󰕥", color = "#8C86AF", cterm_color = "103", name = "Md5" }, + ["mdx"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Mdx" }, + ["mint"] = { icon = "󰌪", color = "#87C095", cterm_color = "108", name = "Mint" }, + ["mjs"] = { icon = "", color = "#F1E05A", cterm_color = "185", name = "Mjs" }, + ["mk"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "Makefile" }, + ["mkv"] = { icon = "", color = "#FD971F", cterm_color = "208", name = "Mkv" }, + ["ml"] = { icon = "", color = "#E37933", cterm_color = "166", name = "Ml" }, + ["mli"] = { icon = "", color = "#E37933", cterm_color = "166", name = "Mli" }, + ["mm"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "ObjectiveCPlusPlus" }, + ["mo"] = { icon = "", color = "#9772FB", cterm_color = "135", name = "Motoko" }, + ["mobi"] = { icon = "", color = "#EAB16D", cterm_color = "215", name = "Mobi" }, + ["mojo"] = { icon = "", color = "#FF4C1F", cterm_color = "196", name = "Mojo" }, + ["mov"] = { icon = "", color = "#FD971F", cterm_color = "208", name = "MOV" }, + ["mp3"] = { icon = "", color = "#00AFFF", cterm_color = "39", name = "MPEGAudioLayerIII" }, + ["mp4"] = { icon = "", color = "#FD971F", cterm_color = "208", name = "Mp4" }, + ["mpp"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Mpp" }, + ["msf"] = { icon = "", color = "#137BE1", cterm_color = "33", name = "Thunderbird" }, + ["mts"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Mts" }, + ["mustache"] = { icon = "", color = "#E37933", cterm_color = "166", name = "Mustache" }, + ["nfo"] = { icon = "", color = "#FFFFCD", cterm_color = "230", name = "Nfo" }, + ["nim"] = { icon = "", color = "#F3D400", cterm_color = "220", name = "Nim" }, + ["nix"] = { icon = "", color = "#7EBAE4", cterm_color = "110", name = "Nix" }, + ["norg"] = { icon = "", color = "#4878BE", cterm_color = "32", name = "Norg" }, + ["nswag"] = { icon = "", color = "#85EA2D", cterm_color = "112", name = "Nswag" }, + ["nu"] = { icon = "", color = "#3AA675", cterm_color = "36", name = "Nushell" }, + ["o"] = { icon = "", color = "#9F0500", cterm_color = "124", name = "ObjectFile" }, + ["obj"] = { icon = "󰆧", color = "#888888", cterm_color = "102", name = "3DObjectFile" }, + ["odf"] = { icon = "", color = "#FF5A96", cterm_color = "204", name = "LibreOfficeFormula" }, + ["odg"] = { icon = "", color = "#FFFB57", cterm_color = "227", name = "LibreOfficeGraphics" }, + ["odin"] = { icon = "󰟢", color = "#3882D2", cterm_color = "32", name = "Odin" }, + ["odp"] = { icon = "", color = "#FE9C45", cterm_color = "215", name = "LibreOfficeImpress" }, + ["ods"] = { icon = "", color = "#78FC4E", cterm_color = "119", name = "LibreOfficeCalc" }, + ["odt"] = { icon = "", color = "#2DCBFD", cterm_color = "81", name = "LibreOfficeWriter" }, + ["oga"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "OggVorbis" }, + ["ogg"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "OggVorbis" }, + ["ogv"] = { icon = "", color = "#FD971F", cterm_color = "208", name = "OggVideo" }, + ["ogx"] = { icon = "", color = "#FD971F", cterm_color = "208", name = "OggMultiplex" }, + ["opus"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "OpusAudioFile" }, + ["org"] = { icon = "", color = "#77AA99", cterm_color = "73", name = "OrgMode" }, + ["otf"] = { icon = "", color = "#ECECEC", cterm_color = "255", name = "OpenTypeFont" }, + ["out"] = { icon = "", color = "#9F0500", cterm_color = "124", name = "Out" }, + ["part"] = { icon = "", color = "#44CDA8", cterm_color = "43", name = "Part" }, + ["patch"] = { icon = "", color = "#41535B", cterm_color = "239", name = "Patch" }, + ["pck"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "PackedResource" }, + ["pcm"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "PulseCodeModulation" }, + ["pdf"] = { icon = "", color = "#B30B00", cterm_color = "124", name = "Pdf" }, + ["php"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Php" }, + ["pl"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Pl" }, + ["pls"] = { icon = "󰲹", color = "#ED95AE", cterm_color = "211", name = "Pls" }, + ["ply"] = { icon = "󰆧", color = "#888888", cterm_color = "102", name = "3DObjectFile" }, + ["pm"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Pm" }, + ["png"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Png" }, + ["po"] = { icon = "", color = "#2596BE", cterm_color = "31", name = "Localization" }, + ["pot"] = { icon = "", color = "#2596BE", cterm_color = "31", name = "Localization" }, + ["pp"] = { icon = "", color = "#FFA61A", cterm_color = "214", name = "Pp" }, + ["ppt"] = { icon = "󰈧", color = "#CB4A32", cterm_color = "160", name = "Ppt" }, + ["pptx"] = { icon = "󰈧", color = "#CB4A32", cterm_color = "160", name = "Pptx" }, + ["prisma"] = { icon = "", color = "#5A67D8", cterm_color = "62", name = "Prisma" }, + ["pro"] = { icon = "", color = "#E4B854", cterm_color = "179", name = "Prolog" }, + ["ps1"] = { icon = "󰨊", color = "#4273CA", cterm_color = "68", name = "PsScriptfile" }, + ["psb"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Psb" }, + ["psd"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Psd" }, + ["psd1"] = { icon = "󰨊", color = "#6975C4", cterm_color = "68", name = "PsManifestfile" }, + ["psm1"] = { icon = "󰨊", color = "#6975C4", cterm_color = "68", name = "PsScriptModulefile" }, + ["pub"] = { icon = "󰷖", color = "#E3C58E", cterm_color = "222", name = "Pub" }, + ["pxd"] = { icon = "", color = "#5AA7E4", cterm_color = "39", name = "Pxd" }, + ["pxi"] = { icon = "", color = "#5AA7E4", cterm_color = "39", name = "Pxi" }, + ["py"] = { icon = "", color = "#FFBC03", cterm_color = "214", name = "Py" }, + ["pyc"] = { icon = "", color = "#FFE291", cterm_color = "222", name = "Pyc" }, + ["pyd"] = { icon = "", color = "#FFE291", cterm_color = "222", name = "Pyd" }, + ["pyi"] = { icon = "", color = "#FFBC03", cterm_color = "214", name = "Pyi" }, + ["pyo"] = { icon = "", color = "#FFE291", cterm_color = "222", name = "Pyo" }, + ["pyw"] = { icon = "", color = "#5AA7E4", cterm_color = "39", name = "Pyw" }, + ["pyx"] = { icon = "", color = "#5AA7E4", cterm_color = "39", name = "Pyx" }, + ["qm"] = { icon = "", color = "#2596BE", cterm_color = "31", name = "Localization" }, + ["qml"] = { icon = "", color = "#40CD52", cterm_color = "77", name = "Qt" }, + ["qrc"] = { icon = "", color = "#40CD52", cterm_color = "77", name = "Qt" }, + ["qss"] = { icon = "", color = "#40CD52", cterm_color = "77", name = "Qt" }, + ["query"] = { icon = "", color = "#90A850", cterm_color = "107", name = "Query" }, + ["r"] = { icon = "󰟔", color = "#2266BA", cterm_color = "25", name = "R" }, + ["rake"] = { icon = "", color = "#701516", cterm_color = "52", name = "Rake" }, + ["rar"] = { icon = "", color = "#ECA517", cterm_color = "214", name = "Rar" }, + ["rasi"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "Rasi" }, + ["razor"] = { icon = "󱦘", color = "#512BD4", cterm_color = "56", name = "RazorPage" }, + ["rb"] = { icon = "", color = "#701516", cterm_color = "52", name = "Rb" }, + ["res"] = { icon = "", color = "#CC3E44", cterm_color = "167", name = "ReScript" }, + ["resi"] = { icon = "", color = "#F55385", cterm_color = "204", name = "ReScriptInterface" }, + ["rlib"] = { icon = "", color = "#DEA584", cterm_color = "216", name = "Rlib" }, + ["rmd"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Rmd" }, + ["rproj"] = { icon = "󰗆", color = "#358A5B", cterm_color = "29", name = "Rproj" }, + ["rs"] = { icon = "", color = "#DEA584", cterm_color = "216", name = "Rs" }, + ["rss"] = { icon = "", color = "#FB9D3B", cterm_color = "215", name = "Rss" }, + ["s"] = { icon = "", color = "#0071C5", cterm_color = "25", name = "ASM" }, + ["sass"] = { icon = "", color = "#F55385", cterm_color = "204", name = "Sass" }, + ["sbt"] = { icon = "", color = "#CC3E44", cterm_color = "167", name = "sbt" }, + ["sc"] = { icon = "", color = "#CC3E44", cterm_color = "167", name = "ScalaScript" }, + ["scad"] = { icon = "", color = "#F9D72C", cterm_color = "220", name = "OpenSCAD" }, + ["scala"] = { icon = "", color = "#CC3E44", cterm_color = "167", name = "Scala" }, + ["scm"] = { icon = "󰘧", color = "#EEEEEE", cterm_color = "255", name = "Scheme" }, + ["scss"] = { icon = "", color = "#F55385", cterm_color = "204", name = "Scss" }, + ["sh"] = { icon = "", color = "#4D5A5E", cterm_color = "240", name = "Sh" }, + ["sha1"] = { icon = "󰕥", color = "#8C86AF", cterm_color = "103", name = "Sha1" }, + ["sha224"] = { icon = "󰕥", color = "#8C86AF", cterm_color = "103", name = "Sha224" }, + ["sha256"] = { icon = "󰕥", color = "#8C86AF", cterm_color = "103", name = "Sha256" }, + ["sha384"] = { icon = "󰕥", color = "#8C86AF", cterm_color = "103", name = "Sha384" }, + ["sha512"] = { icon = "󰕥", color = "#8C86AF", cterm_color = "103", name = "Sha512" }, + ["sig"] = { icon = "󰘧", color = "#E37933", cterm_color = "166", name = "Sig" }, + ["signature"] = { icon = "󰘧", color = "#E37933", cterm_color = "166", name = "Signature" }, + ["skp"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "SketchUp" }, + ["sldasm"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "SolidWorksAsm" }, + ["sldprt"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "SolidWorksPrt" }, + ["slim"] = { icon = "", color = "#E34C26", cterm_color = "196", name = "Slim" }, + ["sln"] = { icon = "", color = "#854CC7", cterm_color = "98", name = "Sln" }, + ["slnx"] = { icon = "", color = "#854CC7", cterm_color = "98", name = "Slnx" }, + ["slvs"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "SolveSpace" }, + ["sml"] = { icon = "󰘧", color = "#E37933", cterm_color = "166", name = "Sml" }, + ["so"] = { icon = "", color = "#DCDDD6", cterm_color = "253", name = "SharedObject" }, + ["sol"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Solidity" }, + ["spec.js"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "SpecJs" }, + ["spec.jsx"] = { icon = "", color = "#20C2E3", cterm_color = "45", name = "JavaScriptReactSpec" }, + ["spec.ts"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "SpecTs" }, + ["spec.tsx"] = { icon = "", color = "#1354BF", cterm_color = "26", name = "TypeScriptReactSpec" }, + ["spx"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "OggSpeexAudio" }, + ["sql"] = { icon = "", color = "#DAD8D8", cterm_color = "188", name = "Sql" }, + ["sqlite"] = { icon = "", color = "#DAD8D8", cterm_color = "188", name = "Sql" }, + ["sqlite3"] = { icon = "", color = "#DAD8D8", cterm_color = "188", name = "Sql" }, + ["srt"] = { icon = "󰨖", color = "#FFB713", cterm_color = "214", name = "Srt" }, + ["ssa"] = { icon = "󰨖", color = "#FFB713", cterm_color = "214", name = "Ssa" }, + ["ste"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "Ste" }, + ["step"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "Step" }, + ["stl"] = { icon = "󰆧", color = "#888888", cterm_color = "102", name = "3DObjectFile" }, + ["stories.js"] = { icon = "", color = "#FF4785", cterm_color = "204", name = "StorybookJavaScript" }, + ["stories.jsx"] = { icon = "", color = "#FF4785", cterm_color = "204", name = "StorybookJsx" }, + ["stories.mjs"] = { icon = "", color = "#FF4785", cterm_color = "204", name = "StorybookMjs" }, + ["stories.svelte"] = { icon = "", color = "#FF4785", cterm_color = "204", name = "StorybookSvelte" }, + ["stories.ts"] = { icon = "", color = "#FF4785", cterm_color = "204", name = "StorybookTypeScript" }, + ["stories.tsx"] = { icon = "", color = "#FF4785", cterm_color = "204", name = "StorybookTsx" }, + ["stories.vue"] = { icon = "", color = "#FF4785", cterm_color = "204", name = "StorybookVue" }, + ["stp"] = { icon = "󰻫", color = "#839463", cterm_color = "101", name = "Stp" }, + ["strings"] = { icon = "", color = "#2596BE", cterm_color = "31", name = "Localization" }, + ["styl"] = { icon = "", color = "#8DC149", cterm_color = "113", name = "Styl" }, + ["sub"] = { icon = "󰨖", color = "#FFB713", cterm_color = "214", name = "Sub" }, + ["sublime"] = { icon = "", color = "#E37933", cterm_color = "166", name = "Sublime" }, + ["suo"] = { icon = "", color = "#854CC7", cterm_color = "98", name = "Suo" }, + ["sv"] = { icon = "󰍛", color = "#019833", cterm_color = "28", name = "SystemVerilog" }, + ["svelte"] = { icon = "", color = "#FF3E00", cterm_color = "196", name = "Svelte" }, + ["svg"] = { icon = "󰜡", color = "#FFB13B", cterm_color = "214", name = "Svg" }, + ["svgz"] = { icon = "󰜡", color = "#FFB13B", cterm_color = "214", name = "Svgz" }, + ["svh"] = { icon = "󰍛", color = "#019833", cterm_color = "28", name = "SystemVerilog" }, + ["swift"] = { icon = "", color = "#E37933", cterm_color = "166", name = "Swift" }, + ["t"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Tor" }, + ["tbc"] = { icon = "󰛓", color = "#1E5CB3", cterm_color = "25", name = "Tcl" }, + ["tcl"] = { icon = "󰛓", color = "#1E5CB3", cterm_color = "25", name = "Tcl" }, + ["templ"] = { icon = "", color = "#DBBD30", cterm_color = "178", name = "Templ" }, + ["terminal"] = { icon = "", color = "#31B53E", cterm_color = "34", name = "Terminal" }, + ["test.js"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "TestJs" }, + ["test.jsx"] = { icon = "", color = "#20C2E3", cterm_color = "45", name = "JavaScriptReactTest" }, + ["test.ts"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "TestTs" }, + ["test.tsx"] = { icon = "", color = "#1354BF", cterm_color = "26", name = "TypeScriptReactTest" }, + ["tex"] = { icon = "", color = "#3D6117", cterm_color = "22", name = "Tex" }, + ["tf"] = { icon = "", color = "#5F43E9", cterm_color = "93", name = "Terraform" }, + ["tfvars"] = { icon = "", color = "#5F43E9", cterm_color = "93", name = "TFVars" }, + ["tgz"] = { icon = "", color = "#ECA517", cterm_color = "214", name = "Tgz" }, + ["tmpl"] = { icon = "", color = "#DBBD30", cterm_color = "178", name = "Template" }, + ["tmux"] = { icon = "", color = "#14BA19", cterm_color = "34", name = "Tmux" }, + ["toml"] = { icon = "", color = "#9C4221", cterm_color = "124", name = "Toml" }, + ["torrent"] = { icon = "", color = "#44CDA8", cterm_color = "43", name = "Torrent" }, + ["tres"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "GodotTextResource" }, + ["ts"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "TypeScript" }, + ["tscn"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "GodotTextScene" }, + ["tsconfig"] = { icon = "", color = "#FF8700", cterm_color = "208", name = "TypoScriptConfig" }, + ["tsx"] = { icon = "", color = "#1354BF", cterm_color = "26", name = "Tsx" }, + ["ttf"] = { icon = "", color = "#ECECEC", cterm_color = "255", name = "TrueTypeFont" }, + ["twig"] = { icon = "", color = "#8DC149", cterm_color = "113", name = "Twig" }, + ["txt"] = { icon = "󰈙", color = "#89E051", cterm_color = "113", name = "Txt" }, + ["txz"] = { icon = "", color = "#ECA517", cterm_color = "214", name = "Txz" }, + ["typ"] = { icon = "", color = "#0DBCC0", cterm_color = "37", name = "Typst" }, + ["typoscript"] = { icon = "", color = "#FF8700", cterm_color = "208", name = "TypoScript" }, + ["ui"] = { icon = "", color = "#015BF0", cterm_color = "27", name = "UI" }, + ["v"] = { icon = "󰍛", color = "#019833", cterm_color = "28", name = "Verilog" }, + ["vala"] = { icon = "", color = "#7B3DB9", cterm_color = "91", name = "Vala" }, + ["vert"] = { icon = "", color = "#5586A6", cterm_color = "67", name = "VertexShader" }, + ["vh"] = { icon = "󰍛", color = "#019833", cterm_color = "28", name = "Verilog" }, + ["vhd"] = { icon = "󰍛", color = "#019833", cterm_color = "28", name = "VHDL" }, + ["vhdl"] = { icon = "󰍛", color = "#019833", cterm_color = "28", name = "VHDL" }, + ["vi"] = { icon = "", color = "#FEC60A", cterm_color = "220", name = "LabView" }, + ["vim"] = { icon = "", color = "#019833", cterm_color = "28", name = "Vim" }, + ["vsh"] = { icon = "", color = "#5D87BF", cterm_color = "67", name = "Vlang" }, + ["vsix"] = { icon = "", color = "#854CC7", cterm_color = "98", name = "Vsix" }, + ["vue"] = { icon = "", color = "#8DC149", cterm_color = "113", name = "Vue" }, + ["wasm"] = { icon = "", color = "#5C4CDB", cterm_color = "62", name = "Wasm" }, + ["wav"] = { icon = "", color = "#00AFFF", cterm_color = "39", name = "WaveformAudioFile" }, + ["webm"] = { icon = "", color = "#FD971F", cterm_color = "208", name = "Webm" }, + ["webmanifest"] = { icon = "", color = "#F1E05A", cterm_color = "185", name = "Webmanifest" }, + ["webp"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Webp" }, + ["webpack"] = { icon = "󰜫", color = "#519ABA", cterm_color = "74", name = "Webpack" }, + ["wma"] = { icon = "", color = "#00AFFF", cterm_color = "39", name = "WindowsMediaAudio" }, + ["wmv"] = { icon = "", color = "#FD971F", cterm_color = "208", name = "WindowsMediaVideo" }, + ["woff"] = { icon = "", color = "#ECECEC", cterm_color = "255", name = "WebOpenFontFormat" }, + ["woff2"] = { icon = "", color = "#ECECEC", cterm_color = "255", name = "WebOpenFontFormat" }, + ["wrl"] = { icon = "󰆧", color = "#888888", cterm_color = "102", name = "VRML" }, + ["wrz"] = { icon = "󰆧", color = "#888888", cterm_color = "102", name = "VRML" }, + ["wv"] = { icon = "", color = "#00AFFF", cterm_color = "39", name = "WavPack" }, + ["wvc"] = { icon = "", color = "#00AFFF", cterm_color = "39", name = "WavPackCorrection" }, + ["x"] = { icon = "", color = "#599EFF", cterm_color = "111", name = "Logos" }, + ["xaml"] = { icon = "󰙳", color = "#512BD4", cterm_color = "56", name = "Xaml" }, + ["xcf"] = { icon = "", color = "#635B46", cterm_color = "240", name = "GIMP" }, + ["xcplayground"] = { icon = "", color = "#E37933", cterm_color = "166", name = "XcPlayground" }, + ["xcstrings"] = { icon = "", color = "#2596BE", cterm_color = "31", name = "XcLocalization" }, + ["xls"] = { icon = "󰈛", color = "#207245", cterm_color = "29", name = "Xls" }, + ["xlsx"] = { icon = "󰈛", color = "#207245", cterm_color = "29", name = "Xlsx" }, + ["xm"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Logos" }, + ["xml"] = { icon = "󰗀", color = "#E37933", cterm_color = "166", name = "Xml" }, + ["xpi"] = { icon = "", color = "#FF1B01", cterm_color = "196", name = "Xpi" }, + ["xul"] = { icon = "", color = "#E37933", cterm_color = "166", name = "Xul" }, + ["xz"] = { icon = "", color = "#ECA517", cterm_color = "214", name = "Xz" }, + ["yaml"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "Yaml" }, + ["yml"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "Yml" }, + ["zig"] = { icon = "", color = "#F69A1B", cterm_color = "172", name = "Zig" }, + ["zip"] = { icon = "", color = "#ECA517", cterm_color = "214", name = "Zip" }, + ["zsh"] = { icon = "", color = "#89E051", cterm_color = "113", name = "Zsh" }, + ["zst"] = { icon = "", color = "#ECA517", cterm_color = "214", name = "Zst" }, + ["🔥"] = { icon = "", color = "#FF4C1F", cterm_color = "196", name = "Mojo" }, +} --[[@as table]] diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_filename.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_filename.lua new file mode 100644 index 0000000..d408744 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_filename.lua @@ -0,0 +1,214 @@ +return { + ["vite.config.cjs"] = { icon = "", color = "#FFA800", cterm_color = "214", name = "ViteConfig" }, + ["vite.config.cts"] = { icon = "", color = "#FFA800", cterm_color = "214", name = "ViteConfig" }, + ["vite.config.js"] = { icon = "", color = "#FFA800", cterm_color = "214", name = "ViteConfig" }, + ["vite.config.mjs"] = { icon = "", color = "#FFA800", cterm_color = "214", name = "ViteConfig" }, + ["vite.config.mts"] = { icon = "", color = "#FFA800", cterm_color = "214", name = "ViteConfig" }, + ["vite.config.ts"] = { icon = "", color = "#FFA800", cterm_color = "214", name = "ViteConfig" }, + [".SRCINFO"] = { icon = "󰣇", color = "#0F94D2", cterm_color = "67", name = "SRCINFO" }, + [".Xauthority"] = { icon = "", color = "#E54D18", cterm_color = "196", name = "Xauthority" }, + [".Xresources"] = { icon = "", color = "#E54D18", cterm_color = "196", name = "Xresources" }, + [".babelrc"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "Babelrc" }, + [".bash_profile"] = { icon = "", color = "#89E051", cterm_color = "113", name = "BashProfile" }, + [".bashrc"] = { icon = "", color = "#89E051", cterm_color = "113", name = "Bashrc" }, + [".clang-format"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "ClangConfig" }, + [".clang-tidy"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "ClangConfig" }, + [".codespellrc"] = { icon = "󰓆", color = "#35DA60", cterm_color = "41", name = "Codespell" }, + [".condarc"] = { icon = "", color = "#43B02A", cterm_color = "34", name = "Conda" }, + [".dockerignore"] = { icon = "󰡨", color = "#458EE6", cterm_color = "68", name = "Dockerfile" }, + [".ds_store"] = { icon = "", color = "#41535B", cterm_color = "239", name = "DsStore" }, + [".editorconfig"] = { icon = "", color = "#FFF2F2", cterm_color = "255", name = "EditorConfig" }, + [".env"] = { icon = "", color = "#FAF743", cterm_color = "227", name = "Env" }, + [".eslintignore"] = { icon = "", color = "#4B32C3", cterm_color = "56", name = "EslintIgnore" }, + [".eslintrc"] = { icon = "", color = "#4B32C3", cterm_color = "56", name = "Eslintrc" }, + [".git-blame-ignore-revs"] = { icon = "", color = "#F54D27", cterm_color = "196", name = "GitBlameIgnore" }, + [".gitattributes"] = { icon = "", color = "#F54D27", cterm_color = "196", name = "GitAttributes" }, + [".gitconfig"] = { icon = "", color = "#F54D27", cterm_color = "196", name = "GitConfig" }, + [".gitignore"] = { icon = "", color = "#F54D27", cterm_color = "196", name = "GitIgnore" }, + [".gitlab-ci.yml"] = { icon = "", color = "#E24329", cterm_color = "196", name = "GitlabCI" }, + [".gitmodules"] = { icon = "", color = "#F54D27", cterm_color = "196", name = "GitModules" }, + [".gtkrc-2.0"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "GTK" }, + [".gvimrc"] = { icon = "", color = "#019833", cterm_color = "28", name = "Gvimrc" }, + [".justfile"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "Justfile" }, + [".luacheckrc"] = { icon = "", color = "#00A2FF", cterm_color = "75", name = "Luaurc" }, + [".luaurc"] = { icon = "", color = "#00A2FF", cterm_color = "75", name = "Luaurc" }, + [".mailmap"] = { icon = "󰊢", color = "#F54D27", cterm_color = "196", name = "Mailmap" }, + [".nanorc"] = { icon = "", color = "#440077", cterm_color = "54", name = "Nano" }, + [".npmignore"] = { icon = "", color = "#E8274B", cterm_color = "197", name = "NPMIgnore" }, + [".npmrc"] = { icon = "", color = "#E8274B", cterm_color = "197", name = "NPMrc" }, + [".nuxtrc"] = { icon = "󱄆", color = "#00C58E", cterm_color = "42", name = "NuxtConfig" }, + [".nvmrc"] = { icon = "", color = "#5FA04E", cterm_color = "71", name = "node" }, + [".pnpmfile.cjs"] = { icon = "", color = "#F9AD02", cterm_color = "214", name = "PNPMFile" }, + [".pre-commit-config.yaml"] = { icon = "󰛢", color = "#F8B424", cterm_color = "214", name = "PreCommitConfig" }, + [".prettierignore"] = { icon = "", color = "#4285F4", cterm_color = "33", name = "PrettierIgnore" }, + [".prettierrc"] = { icon = "", color = "#4285F4", cterm_color = "33", name = "PrettierConfig" }, + [".prettierrc.cjs"] = { icon = "", color = "#4285F4", cterm_color = "33", name = "PrettierConfig" }, + [".prettierrc.js"] = { icon = "", color = "#4285F4", cterm_color = "33", name = "PrettierConfig" }, + [".prettierrc.json"] = { icon = "", color = "#4285F4", cterm_color = "33", name = "PrettierConfig" }, + [".prettierrc.json5"] = { icon = "", color = "#4285F4", cterm_color = "33", name = "PrettierConfig" }, + [".prettierrc.mjs"] = { icon = "", color = "#4285F4", cterm_color = "33", name = "PrettierConfig" }, + [".prettierrc.toml"] = { icon = "", color = "#4285F4", cterm_color = "33", name = "PrettierConfig" }, + [".prettierrc.yaml"] = { icon = "", color = "#4285F4", cterm_color = "33", name = "PrettierConfig" }, + [".prettierrc.yml"] = { icon = "", color = "#4285F4", cterm_color = "33", name = "PrettierConfig" }, + [".pylintrc"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "PyLintConfig" }, + [".settings.json"] = { icon = "", color = "#854CC7", cterm_color = "98", name = "SettingsJson" }, + [".vimrc"] = { icon = "", color = "#019833", cterm_color = "28", name = "Vimrc" }, + [".xinitrc"] = { icon = "", color = "#E54D18", cterm_color = "196", name = "XInitrc" }, + [".xsession"] = { icon = "", color = "#E54D18", cterm_color = "196", name = "Xsession" }, + [".zprofile"] = { icon = "", color = "#89E051", cterm_color = "113", name = "Zshprofile" }, + [".zshenv"] = { icon = "", color = "#89E051", cterm_color = "113", name = "Zshenv" }, + [".zshrc"] = { icon = "", color = "#89E051", cterm_color = "113", name = "Zshrc" }, + ["AUTHORS"] = { icon = "", color = "#A172FF", cterm_color = "135", name = "AUTHORS" }, + ["AUTHORS.txt"] = { icon = "", color = "#A172FF", cterm_color = "135", name = "AUTHORS" }, + ["Directory.Build.props"] = { icon = "", color = "#00A2FF", cterm_color = "75", name = "BuildProps" }, + ["Directory.Build.targets"] = { icon = "", color = "#00A2FF", cterm_color = "75", name = "BuildTargets" }, + ["Directory.Packages.props"] = { icon = "", color = "#00A2FF", cterm_color = "75", name = "PackagesProps" }, + ["FreeCAD.conf"] = { icon = "", color = "#CB333B", cterm_color = "160", name = "FreeCADConfig" }, + ["Gemfile"] = { icon = "", color = "#701516", cterm_color = "52", name = "Gemfile" }, + ["Jenkinsfile"] = { icon = "", color = "#D33833", cterm_color = "160", name = "Jenkins" }, + ["PKGBUILD"] = { icon = "", color = "#0F94D2", cterm_color = "67", name = "PKGBUILD" }, + ["PrusaSlicer.ini"] = { icon = "", color = "#EC6B23", cterm_color = "202", name = "PrusaSlicer" }, + ["PrusaSlicerGcodeViewer.ini"] = { icon = "", color = "#EC6B23", cterm_color = "202", name = "PrusaSlicer" }, + ["QtProject.conf"] = { icon = "", color = "#40CD52", cterm_color = "77", name = "Qt" }, + ["_gvimrc"] = { icon = "", color = "#019833", cterm_color = "28", name = "Gvimrc" }, + ["_vimrc"] = { icon = "", color = "#019833", cterm_color = "28", name = "Vimrc" }, + ["brewfile"] = { icon = "", color = "#701516", cterm_color = "52", name = "Brewfile" }, + ["bspwmrc"] = { icon = "", color = "#2F2F2F", cterm_color = "236", name = "BSPWM" }, + ["build"] = { icon = "", color = "#89E051", cterm_color = "113", name = "BazelBuild" }, + ["build.gradle"] = { icon = "", color = "#005F87", cterm_color = "24", name = "GradleBuildScript" }, + ["build.zig.zon"] = { icon = "", color = "#F69A1B", cterm_color = "172", name = "ZigObjectNotation" }, + ["bun.lock"] = { icon = "", color = "#EADCD1", cterm_color = "253", name = "BunLockfile" }, + ["bun.lockb"] = { icon = "", color = "#EADCD1", cterm_color = "253", name = "BunLockfile" }, + ["cantorrc"] = { icon = "", color = "#1C99F3", cterm_color = "32", name = "Cantorrc" }, + ["checkhealth"] = { icon = "󰓙", color = "#75B4FB", cterm_color = "75", name = "Checkhealth" }, + ["cmakelists.txt"] = { icon = "", color = "#DCE3EB", cterm_color = "254", name = "CMakeLists" }, + ["code_of_conduct"] = { icon = "", color = "#E41662", cterm_color = "161", name = "CodeOfConduct" }, + ["code_of_conduct.md"] = { icon = "", color = "#E41662", cterm_color = "161", name = "CodeOfConduct" }, + ["commit_editmsg"] = { icon = "", color = "#F54D27", cterm_color = "196", name = "GitCommit" }, + ["commitlint.config.js"] = { icon = "󰜘", color = "#2B9689", cterm_color = "30", name = "CommitlintConfig" }, + ["commitlint.config.ts"] = { icon = "󰜘", color = "#2B9689", cterm_color = "30", name = "CommitlintConfig" }, + ["compose.yaml"] = { icon = "󰡨", color = "#458EE6", cterm_color = "68", name = "Dockerfile" }, + ["compose.yml"] = { icon = "󰡨", color = "#458EE6", cterm_color = "68", name = "Dockerfile" }, + ["config"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "Config" }, + ["containerfile"] = { icon = "󰡨", color = "#458EE6", cterm_color = "68", name = "Dockerfile" }, + ["copying"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "License" }, + ["copying.lesser"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "License" }, + ["docker-compose.yaml"] = { icon = "󰡨", color = "#458EE6", cterm_color = "68", name = "Dockerfile" }, + ["docker-compose.yml"] = { icon = "󰡨", color = "#458EE6", cterm_color = "68", name = "Dockerfile" }, + ["dockerfile"] = { icon = "󰡨", color = "#458EE6", cterm_color = "68", name = "Dockerfile" }, + ["eslint.config.cjs"] = { icon = "", color = "#4B32C3", cterm_color = "56", name = "Eslintrc" }, + ["eslint.config.js"] = { icon = "", color = "#4B32C3", cterm_color = "56", name = "Eslintrc" }, + ["eslint.config.mjs"] = { icon = "", color = "#4B32C3", cterm_color = "56", name = "Eslintrc" }, + ["eslint.config.ts"] = { icon = "", color = "#4B32C3", cterm_color = "56", name = "Eslintrc" }, + ["ext_typoscript_setup.txt"] = { icon = "", color = "#FF8700", cterm_color = "208", name = "TypoScriptSetup" }, + ["favicon.ico"] = { icon = "", color = "#CBCB41", cterm_color = "185", name = "Favicon" }, + ["fp-info-cache"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "KiCadCache" }, + ["fp-lib-table"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "KiCadFootprintTable" }, + ["gnumakefile"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "Makefile" }, + ["go.mod"] = { icon = "", color = "#00ADD8", cterm_color = "38", name = "GoMod" }, + ["go.sum"] = { icon = "", color = "#00ADD8", cterm_color = "38", name = "GoSum" }, + ["go.work"] = { icon = "", color = "#00ADD8", cterm_color = "38", name = "GoWork" }, + ["gradle-wrapper.properties"] = { icon = "", color = "#005F87", cterm_color = "24", name = "GradleWrapperProperties" }, + ["gradle.properties"] = { icon = "", color = "#005F87", cterm_color = "24", name = "GradleProperties" }, + ["gradlew"] = { icon = "", color = "#005F87", cterm_color = "24", name = "GradleWrapperScript" }, + ["groovy"] = { icon = "", color = "#4A687C", cterm_color = "24", name = "Groovy" }, + ["gruntfile.babel.js"] = { icon = "", color = "#E37933", cterm_color = "166", name = "Gruntfile" }, + ["gruntfile.coffee"] = { icon = "", color = "#E37933", cterm_color = "166", name = "Gruntfile" }, + ["gruntfile.js"] = { icon = "", color = "#E37933", cterm_color = "166", name = "Gruntfile" }, + ["gruntfile.ts"] = { icon = "", color = "#E37933", cterm_color = "166", name = "Gruntfile" }, + ["gtkrc"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "GTK" }, + ["gulpfile.babel.js"] = { icon = "", color = "#CC3E44", cterm_color = "167", name = "Gulpfile" }, + ["gulpfile.coffee"] = { icon = "", color = "#CC3E44", cterm_color = "167", name = "Gulpfile" }, + ["gulpfile.js"] = { icon = "", color = "#CC3E44", cterm_color = "167", name = "Gulpfile" }, + ["gulpfile.ts"] = { icon = "", color = "#CC3E44", cterm_color = "167", name = "Gulpfile" }, + ["hypridle.conf"] = { icon = "", color = "#00AAAE", cterm_color = "37", name = "Hypridle" }, + ["hyprland.conf"] = { icon = "", color = "#00AAAE", cterm_color = "37", name = "Hyprland" }, + ["hyprlandd.conf"] = { icon = "", color = "#00AAAE", cterm_color = "37", name = "Hyprlandd" }, + ["hyprlock.conf"] = { icon = "", color = "#00AAAE", cterm_color = "37", name = "Hyprlock" }, + ["hyprpaper.conf"] = { icon = "", color = "#00AAAE", cterm_color = "37", name = "Hyprpaper" }, + ["hyprsunset.conf"] = { icon = "", color = "#00AAAE", cterm_color = "37", name = "Hyprsunset" }, + ["i18n.config.js"] = { icon = "󰗊", color = "#7986CB", cterm_color = "104", name = "I18nConfig" }, + ["i18n.config.ts"] = { icon = "󰗊", color = "#7986CB", cterm_color = "104", name = "I18nConfig" }, + ["i3blocks.conf"] = { icon = "", color = "#E8EBEE", cterm_color = "255", name = "i3" }, + ["i3status.conf"] = { icon = "", color = "#E8EBEE", cterm_color = "255", name = "i3" }, + ["index.theme"] = { icon = "", color = "#2DB96F", cterm_color = "35", name = "IndexTheme" }, + ["ionic.config.json"] = { icon = "", color = "#4F8FF7", cterm_color = "33", name = "Ionic" }, + ["justfile"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "Justfile" }, + ["kalgebrarc"] = { icon = "", color = "#1C99F3", cterm_color = "32", name = "Kalgebrarc" }, + ["kdeglobals"] = { icon = "", color = "#1C99F3", cterm_color = "32", name = "KDEglobals" }, + ["kdenlive-layoutsrc"] = { icon = "", color = "#83B8F2", cterm_color = "110", name = "KdenliveLayoutsrc" }, + ["kdenliverc"] = { icon = "", color = "#83B8F2", cterm_color = "110", name = "Kdenliverc" }, + ["kritadisplayrc"] = { icon = "", color = "#F245FB", cterm_color = "201", name = "Kritadisplayrc" }, + ["kritarc"] = { icon = "", color = "#F245FB", cterm_color = "201", name = "Kritarc" }, + ["license"] = { icon = "", color = "#D0BF41", cterm_color = "185", name = "License" }, + ["license.md"] = { icon = "", color = "#D0BF41", cterm_color = "185", name = "License" }, + ["lxde-rc.xml"] = { icon = "", color = "#909090", cterm_color = "246", name = "LXDEConfigFile" }, + ["lxqt.conf"] = { icon = "", color = "#0192D3", cterm_color = "32", name = "LXQtConfigFile" }, + ["makefile"] = { icon = "", color = "#6D8086", cterm_color = "66", name = "Makefile" }, + ["mix.lock"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "MixLock" }, + ["mpv.conf"] = { icon = "", color = "#3B1342", cterm_color = "53", name = "Mpv" }, + ["next.config.cjs"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "NextConfigCjs" }, + ["next.config.js"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "NextConfigJs" }, + ["next.config.ts"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "NextConfigTs" }, + ["node_modules"] = { icon = "", color = "#E8274B", cterm_color = "197", name = "NodeModules" }, + ["nuxt.config.cjs"] = { icon = "󱄆", color = "#00C58E", cterm_color = "42", name = "NuxtConfig" }, + ["nuxt.config.js"] = { icon = "󱄆", color = "#00C58E", cterm_color = "42", name = "NuxtConfig" }, + ["nuxt.config.mjs"] = { icon = "󱄆", color = "#00C58E", cterm_color = "42", name = "NuxtConfig" }, + ["nuxt.config.ts"] = { icon = "󱄆", color = "#00C58E", cterm_color = "42", name = "NuxtConfig" }, + ["package-lock.json"] = { icon = "", color = "#7A0D21", cterm_color = "52", name = "PackageLockJson" }, + ["package.json"] = { icon = "", color = "#E8274B", cterm_color = "197", name = "PackageJson" }, + ["platformio.ini"] = { icon = "", color = "#F6822B", cterm_color = "208", name = "Platformio" }, + ["playwright.config.cjs"] = { icon = "", color = "#2FAD33", cterm_color = "34", name = "PlaywrightConfig" }, + ["playwright.config.cts"] = { icon = "", color = "#2FAD33", cterm_color = "34", name = "PlaywrightConfig" }, + ["playwright.config.js"] = { icon = "", color = "#2FAD33", cterm_color = "34", name = "PlaywrightConfig" }, + ["playwright.config.mjs"] = { icon = "", color = "#2FAD33", cterm_color = "34", name = "PlaywrightConfig" }, + ["playwright.config.mts"] = { icon = "", color = "#2FAD33", cterm_color = "34", name = "PlaywrightConfig" }, + ["playwright.config.ts"] = { icon = "", color = "#2FAD33", cterm_color = "34", name = "PlaywrightConfig" }, + ["pnpm-lock.yaml"] = { icon = "", color = "#F9AD02", cterm_color = "214", name = "PNPMLock" }, + ["pnpm-workspace.yaml"] = { icon = "", color = "#F9AD02", cterm_color = "214", name = "PNPMWorkspace" }, + ["pom.xml"] = { icon = "", color = "#7A0D21", cterm_color = "52", name = "Maven" }, + ["prettier.config.cjs"] = { icon = "", color = "#4285F4", cterm_color = "33", name = "PrettierConfig" }, + ["prettier.config.js"] = { icon = "", color = "#4285F4", cterm_color = "33", name = "PrettierConfig" }, + ["prettier.config.mjs"] = { icon = "", color = "#4285F4", cterm_color = "33", name = "PrettierConfig" }, + ["prettier.config.ts"] = { icon = "", color = "#4285F4", cterm_color = "33", name = "PrettierConfig" }, + ["procfile"] = { icon = "", color = "#A074C4", cterm_color = "140", name = "Procfile" }, + ["py.typed"] = { icon = "", color = "#FFBC03", cterm_color = "214", name = "Py.typed" }, + ["rakefile"] = { icon = "", color = "#701516", cterm_color = "52", name = "Rakefile" }, + ["readme"] = { icon = "󰂺", color = "#EDEDED", cterm_color = "255", name = "Readme" }, + ["readme.md"] = { icon = "󰂺", color = "#EDEDED", cterm_color = "255", name = "Readme" }, + ["rmd"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "Rmd" }, + ["robots.txt"] = { icon = "󰚩", color = "#5D7096", cterm_color = "60", name = "RobotsTxt" }, + ["security"] = { icon = "󰒃", color = "#BEC4C9", cterm_color = "251", name = "Security" }, + ["security.md"] = { icon = "󰒃", color = "#BEC4C9", cterm_color = "251", name = "Security" }, + ["settings.gradle"] = { icon = "", color = "#005F87", cterm_color = "24", name = "GradleSettings" }, + ["svelte.config.js"] = { icon = "", color = "#FF3E00", cterm_color = "196", name = "SvelteConfig" }, + ["sxhkdrc"] = { icon = "", color = "#2F2F2F", cterm_color = "236", name = "BSPWM" }, + ["sym-lib-table"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "KiCadSymbolTable" }, + ["tailwind.config.js"] = { icon = "󱏿", color = "#20C2E3", cterm_color = "45", name = "TailwindConfig" }, + ["tailwind.config.mjs"] = { icon = "󱏿", color = "#20C2E3", cterm_color = "45", name = "TailwindConfig" }, + ["tailwind.config.ts"] = { icon = "󱏿", color = "#20C2E3", cterm_color = "45", name = "TailwindConfig" }, + ["tmux.conf"] = { icon = "", color = "#14BA19", cterm_color = "34", name = "Tmux" }, + ["tmux.conf.local"] = { icon = "", color = "#14BA19", cterm_color = "34", name = "Tmux" }, + ["tsconfig.json"] = { icon = "", color = "#519ABA", cterm_color = "74", name = "TSConfig" }, + ["unlicense"] = { icon = "", color = "#D0BF41", cterm_color = "185", name = "License" }, + ["vagrantfile"] = { icon = "", color = "#1563FF", cterm_color = "27", name = "Vagrantfile" }, + ["vercel.json"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "Vercel" }, + ["vitest.config.cjs"] = { icon = "", color = "#739B1B", cterm_color = "106", name = "VitestConfig" }, + ["vitest.config.cts"] = { icon = "", color = "#739B1B", cterm_color = "106", name = "VitestConfig" }, + ["vitest.config.js"] = { icon = "", color = "#739B1B", cterm_color = "106", name = "VitestConfig" }, + ["vitest.config.mjs"] = { icon = "", color = "#739B1B", cterm_color = "106", name = "VitestConfig" }, + ["vitest.config.mts"] = { icon = "", color = "#739B1B", cterm_color = "106", name = "VitestConfig" }, + ["vitest.config.ts"] = { icon = "", color = "#739B1B", cterm_color = "106", name = "VitestConfig" }, + ["vlcrc"] = { icon = "󰕼", color = "#EE7A00", cterm_color = "208", name = "VLC" }, + ["webpack"] = { icon = "󰜫", color = "#519ABA", cterm_color = "74", name = "Webpack" }, + ["weston.ini"] = { icon = "", color = "#FFBB01", cterm_color = "214", name = "Weston" }, + ["workspace"] = { icon = "", color = "#89E051", cterm_color = "113", name = "BazelWorkspace" }, + ["wrangler.jsonc"] = { icon = "", color = "#F48120", cterm_color = "208", name = "WranglerConfig" }, + ["wrangler.toml"] = { icon = "", color = "#F48120", cterm_color = "208", name = "WranglerConfig" }, + ["xdph.conf"] = { icon = "", color = "#00AAAE", cterm_color = "37", name = "XDPH" }, + ["xmobarrc"] = { icon = "", color = "#FD4D5D", cterm_color = "203", name = "xmonad" }, + ["xmobarrc.hs"] = { icon = "", color = "#FD4D5D", cterm_color = "203", name = "xmonad" }, + ["xmonad.hs"] = { icon = "", color = "#FD4D5D", cterm_color = "203", name = "xmonad" }, + ["xorg.conf"] = { icon = "", color = "#E54D18", cterm_color = "196", name = "XorgConf" }, + ["xsettingsd.conf"] = { icon = "", color = "#E54D18", cterm_color = "196", name = "XSettingsdConf" }, +} --[[@as table]] diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_operating_system.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_operating_system.lua new file mode 100644 index 0000000..41ccee2 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_operating_system.lua @@ -0,0 +1,62 @@ +return { + ["alma"] = { icon = "", color = "#FF4649", cterm_color = "203", name = "Almalinux" }, + ["alpine"] = { icon = "", color = "#0D597F", cterm_color = "24", name = "Alpine" }, + ["aosc"] = { icon = "", color = "#C00000", cterm_color = "124", name = "AOSC" }, + ["apple"] = { icon = "", color = "#A2AAAD", cterm_color = "248", name = "Apple" }, + ["arch"] = { icon = "󰣇", color = "#0F94D2", cterm_color = "67", name = "Arch" }, + ["archcraft"] = { icon = "", color = "#86BBA3", cterm_color = "108", name = "Archcraft" }, + ["archlabs"] = { icon = "", color = "#503F42", cterm_color = "238", name = "Archlabs" }, + ["arcolinux"] = { icon = "", color = "#6690EB", cterm_color = "68", name = "ArcoLinux" }, + ["artix"] = { icon = "", color = "#41B4D7", cterm_color = "38", name = "Artix" }, + ["biglinux"] = { icon = "", color = "#189FC8", cterm_color = "38", name = "BigLinux" }, + ["centos"] = { icon = "", color = "#A2518D", cterm_color = "132", name = "Centos" }, + ["crystallinux"] = { icon = "", color = "#A900FF", cterm_color = "129", name = "CrystalLinux" }, + ["debian"] = { icon = "", color = "#A80030", cterm_color = "88", name = "Debian" }, + ["deepin"] = { icon = "", color = "#2CA7F8", cterm_color = "39", name = "Deepin" }, + ["devuan"] = { icon = "", color = "#404A52", cterm_color = "238", name = "Devuan" }, + ["elementary"] = { icon = "", color = "#5890C2", cterm_color = "67", name = "Elementary" }, + ["endeavour"] = { icon = "", color = "#7B3DB9", cterm_color = "91", name = "Endeavour" }, + ["fedora"] = { icon = "", color = "#072A5E", cterm_color = "17", name = "Fedora" }, + ["freebsd"] = { icon = "", color = "#C90F02", cterm_color = "160", name = "FreeBSD" }, + ["garuda"] = { icon = "", color = "#2974E1", cterm_color = "33", name = "GarudaLinux" }, + ["gentoo"] = { icon = "󰣨", color = "#B1ABCE", cterm_color = "146", name = "Gentoo" }, + ["guix"] = { icon = "", color = "#FFCC00", cterm_color = "220", name = "Guix" }, + ["hyperbola"] = { icon = "", color = "#C0C0C0", cterm_color = "250", name = "HyperbolaGNULinuxLibre" }, + ["illumos"] = { icon = "", color = "#FF430F", cterm_color = "196", name = "Illumos" }, + ["kali"] = { icon = "", color = "#2777FF", cterm_color = "69", name = "Kali" }, + ["kdeneon"] = { icon = "", color = "#20A6A4", cterm_color = "37", name = "KDEneon" }, + ["kubuntu"] = { icon = "", color = "#007AC2", cterm_color = "32", name = "Kubuntu" }, + ["leap"] = { icon = "", color = "#FBC75D", cterm_color = "221", name = "Leap" }, + ["linux"] = { icon = "", color = "#FDFDFB", cterm_color = "231", name = "Linux" }, + ["locos"] = { icon = "", color = "#FAB402", cterm_color = "214", name = "LocOS" }, + ["lxle"] = { icon = "", color = "#474747", cterm_color = "238", name = "LXLE" }, + ["mageia"] = { icon = "", color = "#2397D4", cterm_color = "67", name = "Mageia" }, + ["manjaro"] = { icon = "", color = "#33B959", cterm_color = "35", name = "Manjaro" }, + ["mint"] = { icon = "󰣭", color = "#66AF3D", cterm_color = "70", name = "Mint" }, + ["mxlinux"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "MXLinux" }, + ["nixos"] = { icon = "", color = "#7AB1DB", cterm_color = "110", name = "NixOS" }, + ["nobara"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "NobaraLinux" }, + ["openbsd"] = { icon = "", color = "#F2CA30", cterm_color = "220", name = "OpenBSD" }, + ["opensuse"] = { icon = "", color = "#6FB424", cterm_color = "70", name = "openSUSE" }, + ["parabola"] = { icon = "", color = "#797DAC", cterm_color = "103", name = "ParabolaGNULinuxLibre" }, + ["parrot"] = { icon = "", color = "#54DEFF", cterm_color = "45", name = "Parrot" }, + ["pop_os"] = { icon = "", color = "#48B9C7", cterm_color = "73", name = "Pop_OS" }, + ["postmarketos"] = { icon = "", color = "#009900", cterm_color = "28", name = "postmarketOS" }, + ["puppylinux"] = { icon = "", color = "#A2AEB9", cterm_color = "145", name = "PuppyLinux" }, + ["qubesos"] = { icon = "", color = "#3774D8", cterm_color = "33", name = "QubesOS" }, + ["raspberry_pi"] = { icon = "", color = "#BE1848", cterm_color = "161", name = "RaspberryPiOS" }, + ["redhat"] = { icon = "󱄛", color = "#EE0000", cterm_color = "196", name = "Redhat" }, + ["rocky"] = { icon = "", color = "#0FB37D", cterm_color = "36", name = "RockyLinux" }, + ["sabayon"] = { icon = "", color = "#C6C6C6", cterm_color = "251", name = "Sabayon" }, + ["slackware"] = { icon = "", color = "#475FA9", cterm_color = "61", name = "Slackware" }, + ["solus"] = { icon = "", color = "#4B5163", cterm_color = "239", name = "Solus" }, + ["tails"] = { icon = "", color = "#56347C", cterm_color = "54", name = "Tails" }, + ["trisquel"] = { icon = "", color = "#0F58B6", cterm_color = "25", name = "TrisquelGNULinux" }, + ["tumbleweed"] = { icon = "", color = "#35B9AB", cterm_color = "37", name = "Tumbleweed" }, + ["ubuntu"] = { icon = "", color = "#DD4814", cterm_color = "196", name = "Ubuntu" }, + ["vanillaos"] = { icon = "", color = "#FABD4D", cterm_color = "214", name = "VanillaOS" }, + ["void"] = { icon = "", color = "#295340", cterm_color = "23", name = "Void" }, + ["windows"] = { icon = "", color = "#00A4EF", cterm_color = "39", name = "Windows" }, + ["xerolinux"] = { icon = "", color = "#888FE2", cterm_color = "104", name = "XeroLinux" }, + ["zorin"] = { icon = "", color = "#14A1E8", cterm_color = "39", name = "Zorin" }, +} --[[@as table]] diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_window_manager.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_window_manager.lua new file mode 100644 index 0000000..2c39601 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/default/icons_by_window_manager.lua @@ -0,0 +1,14 @@ +return { + ["awesomewm"] = { icon = "", color = "#535D6C", cterm_color = "59", name = "awesome" }, + ["bspwm"] = { icon = "", color = "#4F4F4F", cterm_color = "239", name = "BSPWM" }, + ["dwm"] = { icon = "", color = "#1177AA", cterm_color = "31", name = "dwm" }, + ["enlightenment"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "Enlightenment" }, + ["fluxbox"] = { icon = "", color = "#555555", cterm_color = "240", name = "Fluxbox" }, + ["hyprland"] = { icon = "", color = "#00AAAE", cterm_color = "37", name = "Hyprland" }, + ["i3"] = { icon = "", color = "#E8EBEE", cterm_color = "255", name = "i3" }, + ["jwm"] = { icon = "", color = "#0078CD", cterm_color = "32", name = "JWM" }, + ["qtile"] = { icon = "", color = "#FFFFFF", cterm_color = "231", name = "Qtile" }, + ["river"] = { icon = "", color = "#000000", cterm_color = "16", name = "river" }, + ["sway"] = { icon = "", color = "#68751C", cterm_color = "64", name = "Sway" }, + ["xmonad"] = { icon = "", color = "#FD4D5D", cterm_color = "203", name = "xmonad" }, +} --[[@as table]] diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/filetypes.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/filetypes.lua new file mode 100644 index 0000000..54ff487 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/filetypes.lua @@ -0,0 +1,231 @@ +---Maps filetypes to icon names +---@type table +return { + ["ada"] = "ada", + ["apl"] = "apl", + ["asm"] = "asm", + ["astro"] = "astro", + ["avif"] = "avif", + ["awk"] = "awk", + ["bash"] = "bash", + ["bib"] = "bib", + ["bicep"] = "bicep", + ["bicepparam"] = "bicepparam", + ["blueprint"] = "blp", + ["bmp"] = "bmp", + ["bqn"] = "bqn", + ["brewfile"] = "brewfile", + ["bzl"] = "bzl", + ["c"] = "c", + ["cfg"] = "cfg", + ["checkhealth"] = "checkhealth", + ["clojure"] = "clj", + ["cmake"] = "cmake", + ["cobol"] = "cobol", + ["coffee"] = "coffee", + ["commit"] = "commit_editmsg", + ["conf"] = "conf", + ["copying"] = "copying", + ["cp"] = "cp", + ["cpp"] = "cpp", + ["cr"] = "cr", + ["cs"] = "cs", + ["csh"] = "csh", + ["cson"] = "cson", + ["css"] = "css", + ["csv"] = "csv", + ["cuda"] = "cu", + ["d"] = "d", + ["dart"] = "dart", + ["desktop"] = "desktop", + ["diff"] = "diff", + ["doc"] = "doc", + ["dockerfile"] = "dockerfile", + ["docx"] = "docx", + ["dosbatch"] = "bat", + ["dosini"] = "ini", + ["dot"] = "dot", + ["drools"] = "drl", + ["dropbox"] = "dropbox", + ["dump"] = "dump", + ["editorconfig"] = ".editorconfig", + ["eex"] = "eex", + ["ejs"] = "ejs", + ["elf"] = "elf", + ["elixir"] = "ex", + ["elm"] = "elm", + ["epuppet"] = "epp", + ["erlang"] = "erl", + ["eruby"] = "erb", + ["fennel"] = "fnl", + ["fish"] = "fish", + ["forth"] = "fs", + ["fortran"] = "f90", + ["fsharp"] = "f#", + ["fsi"] = "fsi", + ["fsscript"] = "fsscript", + ["fsx"] = "fsx", + ["gd"] = "gd", + ["gemfile"] = "gemfile$", + ["gif"] = "gif", + ["git"] = "git", + ["gitattributes"] = ".gitattributes", + ["gitcommit"] = "commit_editmsg", + ["gitconfig"] = ".gitconfig", + ["gitignore"] = ".gitignore", + ["glb"] = "glb", + ["glsl"] = "glsl", + ["go"] = "go", + ["godot"] = "godot", + ["gql"] = "gql", + ["graphql"] = "graphql", + ["groovy"] = "groovy", + ["gruntfile"] = "gruntfile", + ["gtkrc"] = "gtkrc", + ["gulpfile"] = "gulpfile", + ["haml"] = "haml", + ["haskell"] = "hs", + ["haxe"] = "hx", + ["hbs"] = "hbs", + ["heex"] = "heex", + ["hex"] = "hex", + ["html"] = "html", + ["http"] = "http", + ["hyprlang"] = "hyprland", + ["ico"] = "ico", + ["idlang"] = "pro", + ["import"] = "import", + ["ino"] = "ino", + ["ipynb"] = "ipynb", + ["java"] = "java", + ["javascript"] = "js", + ["javascript.jsx"] = "jsx", + ["javascriptreact"] = "jsx", + ["jpeg"] = "jpeg", + ["jpg"] = "jpg", + ["json"] = "json", + ["json5"] = "json5", + ["jsonc"] = "jsonc", + ["julia"] = "jl", + ["kotlin"] = "kt", + ["leex"] = "leex", + ["less"] = "less", + ["lesser"] = "copying.lesser", + ["lhaskell"] = "lhs", + ["license"] = "license", + ["liquid"] = "liquid", + ["lock"] = "lock", + ["log"] = "log", + ["lprolog"] = "sig", + ["lua"] = "lua", + ["luau"] = "luau", + ["make"] = "makefile", + ["markdown"] = "markdown", + ["material"] = "material", + ["mdx"] = "mdx", + ["mint"] = "mint", + ["mojo"] = "mojo", + ["motoko"] = "mo", + ["mustache"] = "mustache", + ["nim"] = "nim", + ["nix"] = "nix", + ["node"] = "node_modules", + ["norg"] = "norg", + ["nu"] = "nu", + ["obj"] = "obj", + ["ocaml"] = "ml", + ["odin"] = "odin", + ["openscad"] = "scad", + ["opus"] = "opus", + ["org"] = "org", + ["otf"] = "otf", + ["pck"] = "pck", + ["pdf"] = "pdf", + ["perl"] = "pl", + ["php"] = "php", + ["plaintex"] = "tex", + ["png"] = "png", + ["po"] = "po", + ["postscr"] = "ai", + ["ppt"] = "ppt", + ["prisma"] = "prisma", + ["procfile"] = "procfile", + ["prolog"] = "pro", + ["ps1"] = "ps1", + ["psb"] = "psb", + ["psd"] = "psd", + ["psd1"] = "psd1", + ["psm1"] = "psm1", + ["puppet"] = "pp", + ["pyc"] = "pyc", + ["pyd"] = "pyd", + ["pyo"] = "pyo", + ["python"] = "py", + ["qml"] = "qml", + ["query"] = "query", + ["r"] = "r", + ["rasi"] = "rasi", + ["res"] = "rescript", + ["resi"] = "rescript", + ["rlib"] = "rlib", + ["rmd"] = "rmd", + ["rproj"] = "rproj", + ["ruby"] = "rb", + ["rust"] = "rs", + ["sass"] = "sass", + ["sbt"] = "sbt", + ["scala"] = "scala", + ["scheme"] = "scm", + ["scss"] = "scss", + ["sh"] = "sh", + ["slim"] = "slim", + ["sln"] = "sln", + ["sml"] = "sml", + ["solidity"] = "sol", + ["sql"] = "sql", + ["sqlite"] = "sqlite", + ["sqlite3"] = "sqlite3", + ["srt"] = "srt", + ["ssa"] = "ssa", + ["stp"] = "stp", + ["styl"] = "styl", + ["sublime"] = "sublime", + ["suo"] = "suo", + ["svelte"] = "svelte", + ["svg"] = "svg", + ["swift"] = "swift", + ["systemverilog"] = "sv", + ["tads"] = "t", + ["tcl"] = "tcl", + ["templ"] = "templ", + ["terminal"] = "terminal", + ["tex"] = "tex", + ["text"] = "txt", + ["tf"] = "tf", + ["tmux"] = "tmux", + ["toml"] = "toml", + ["tres"] = "tres", + ["tscn"] = "tscn", + ["twig"] = "twig", + ["typescript"] = "ts", + ["typescriptreact"] = "tsx", + ["typst"] = "typ", + ["unlicense"] = "unlicense", + ["vagrantfile"] = "vagrantfile$", + ["vala"] = "vala", + ["verilog"] = "v", + ["vhdl"] = "vhd", + ["vim"] = "vim", + ["vue"] = "vue", + ["wasm"] = "wasm", + ["webm"] = "webm", + ["webp"] = "webp", + ["webpack"] = "webpack", + ["xcplayground"] = "xcplayground", + ["xls"] = "xls", + ["xlsx"] = "xlsx", + ["xml"] = "xml", + ["yaml"] = "yaml", + ["zig"] = "zig", + ["zsh"] = "zsh", +} diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/hi-test.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/hi-test.lua new file mode 100644 index 0000000..c4c8b67 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/hi-test.lua @@ -0,0 +1,147 @@ +---Run a test similar to :so $VIMRUNTIME/syntax/hitest.vim +---Display all icons and their group highlighted, followed by the concrete definition + +local namespace_hi_test_id = vim.api.nvim_create_namespace "NvimWebDeviconsHiTest" + +---@class (exact) IconDisplay for :NvimTreeHiTest +---@field tag string filename, os or extension +---@field name string name without prefix +---@field icon string icon itself +---@field group string|nil :hi group name +---@field def string|nil :hi concrete definition +---@field private __index IconDisplay? TODO migrate to classic +local IconDisplay = {} + +---@param o IconDisplay +---@return IconDisplay|nil +function IconDisplay:new(o) + if type(o.tag) ~= "string" or type(o.name) ~= "string" or type(o.icon) ~= "string" then + return nil + end + + setmetatable(o, self) + self.__index = self + + o.group = "DevIcon" .. o.name + o.tag = o.tag or "" + + -- concrete definition + local ok, res = pcall(vim.api.nvim_cmd, { cmd = "highlight", args = { o.group } }, { output = true }) + if ok and type(res) == "string" then + o.def = res:gsub(".*xxx *", "") + else + o.def = "" + end + + return o +end + +---Write the line with highlighting +---@param bufnr number buffer number +---@param max_tag_len number longest tag length +---@param max_group_len number longest group length +---@param l number line number +---@return number l incremented +function IconDisplay:render(bufnr, max_tag_len, max_group_len, l) + local fmt = string.format("%%s %%-%d.%ds %%-%d.%ds %%s", max_tag_len, max_tag_len, max_group_len, max_group_len) + local text = string.format(fmt, self.icon, self.tag, self.group, self.def) + + vim.api.nvim_buf_set_lines(bufnr, l, -1, true, { text }) + if vim.fn.has "nvim-0.11" == 1 and vim.hl and vim.hl.range then + vim.hl.range(bufnr, namespace_hi_test_id, self.group, { l, 0 }, { l, -1 }, {}) + else + vim.api.nvim_buf_add_highlight(bufnr, -1, self.group, l, 0, -1) ---@diagnostic disable-line: deprecated + end + + return l + 1 +end + +---Render a single line of text +---@param bufnr number +---@param l number line number +---@return number l incremented +local function render_line(bufnr, l, text) + vim.api.nvim_buf_set_lines(bufnr, l, -1, true, { text }) + return l + 1 +end + +---Render all icons sorted by tag +---@param bufnr number +---@param l number line number +---@param icons table +---@param header string +---@return number l incremented +local function render_icons(bufnr, l, icons, header) + local max_tag_len = 0 + local max_group_len = 0 + + local displays = {} + ---@cast displays IconDisplay[] + + -- build all icon displays + for tag, icon in pairs(icons) do + local display = IconDisplay:new { tag = tag, name = icon.name, icon = icon.icon } + if display then + table.insert(displays, display) + max_tag_len = math.max(max_tag_len, #display.tag) + max_group_len = math.max(max_group_len, #display.group) + end + end + + -- sort by name + table.sort(displays, function(a, b) + return a.name < b.name + end) + + l = render_line(bufnr, l, header) + l = render_line(bufnr, l, header:gsub(".", "-")) + for _, display in ipairs(displays) do + l = display:render(bufnr, max_tag_len, max_group_len, l) + end + l = render_line(bufnr, l, "") + + return l +end + +---Create a buffer similar to :ru syntax/hitest.vim displaying each set icons +---Icon, name, , concrete highlight definition +---tag and header follows param +---@param default_icon table no tag "Default" +---@param global_override table[] all global overrides "Overrides" +---@param icons_by_filename table[] filename "By File Name" +---@param icons_by_file_extension table[] extension "By File Extension" +---@param icons_by_operating_system table[] os "By Operating System" +---@param icons_by_desktop_environment table[] os "By Desktop Environment" +---@param icons_by_window_manager table[] os "By Window Manager" +return function( + default_icon, + global_override, + icons_by_filename, + icons_by_file_extension, + icons_by_operating_system, + icons_by_desktop_environment, + icons_by_window_manager +) + -- create a buffer + local bufnr = vim.api.nvim_create_buf(false, true) + + -- render and highlight each section + local l = 0 + l = render_icons(bufnr, l, { [""] = default_icon }, "Default") + if global_override and next(global_override) then + l = render_icons(bufnr, l, global_override, "Overrides") + end + l = render_icons(bufnr, l, icons_by_filename, "By File Name") + l = render_icons(bufnr, l, icons_by_file_extension, "By File Extension") + l = render_icons(bufnr, l, icons_by_operating_system, "By Operating System") + l = render_icons(bufnr, l, icons_by_desktop_environment, "By Desktop Environment") + render_icons(bufnr, l, icons_by_window_manager, "By Window Manager") + + -- finalise and focus the buffer + if vim.fn.has "nvim-0.10" == 1 then + vim.api.nvim_set_option_value("modifiable", false, { buf = bufnr }) + else + vim.api.nvim_buf_set_option(bufnr, "modifiable", false) ---@diagnostic disable-line: deprecated + end + vim.cmd.buffer(bufnr) +end diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/icons-default.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/icons-default.lua new file mode 100644 index 0000000..5cd10f4 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/icons-default.lua @@ -0,0 +1,7 @@ +return { + icons_by_filename = require "nvim-web-devicons.default.icons_by_filename", + icons_by_file_extension = require "nvim-web-devicons.default.icons_by_file_extension", + icons_by_operating_system = require "nvim-web-devicons.default.icons_by_operating_system", + icons_by_desktop_environment = require "nvim-web-devicons.default.icons_by_desktop_environment", + icons_by_window_manager = require "nvim-web-devicons.default.icons_by_window_manager", +} diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/icons-light.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/icons-light.lua new file mode 100644 index 0000000..18e6280 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/icons-light.lua @@ -0,0 +1,7 @@ +return { + icons_by_filename = require "nvim-web-devicons.light.icons_by_filename", + icons_by_file_extension = require "nvim-web-devicons.light.icons_by_file_extension", + icons_by_operating_system = require "nvim-web-devicons.light.icons_by_operating_system", + icons_by_desktop_environment = require "nvim-web-devicons.light.icons_by_desktop_environment", + icons_by_window_manager = require "nvim-web-devicons.light.icons_by_window_manager", +} diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_desktop_environment.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_desktop_environment.lua new file mode 100644 index 0000000..116effa --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_desktop_environment.lua @@ -0,0 +1,10 @@ +return { -- this file is generated from lua/nvim-web-devicons/default/icons_by_desktop_environment.lua, please do not edit + ["budgie"] = { icon = "", color = "#4E5361", cterm_color = "240", name = "Budgie" }, + ["cinnamon"] = { icon = "", color = "#93451F", cterm_color = "124", name = "Cinnamon" }, + ["gnome"] = { icon = "", color = "#333333", cterm_color = "236", name = "GNOME" }, + ["lxde"] = { icon = "", color = "#525252", cterm_color = "239", name = "LXDE" }, + ["lxqt"] = { icon = "", color = "#016D9E", cterm_color = "24", name = "LXQt" }, + ["mate"] = { icon = "", color = "#4E6D2E", cterm_color = "22", name = "MATE" }, + ["plasma"] = { icon = "", color = "#1467B7", cterm_color = "25", name = "KDEPlasma" }, + ["xfce"] = { icon = "", color = "#0080A7", cterm_color = "31", name = "Xfce" }, +} --[[@as table]] diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_file_extension.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_file_extension.lua new file mode 100644 index 0000000..b0c3459 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_file_extension.lua @@ -0,0 +1,487 @@ +return { -- this file is generated from lua/nvim-web-devicons/default/icons_by_file_extension.lua, please do not edit + + ["3gp"] = { icon = "", color = "#7E4C10", cterm_color = "94", name = "3gp" }, + ["3mf"] = { icon = "󰆧", color = "#5B5B5B", cterm_color = "240", name = "3DObjectFile" }, + ["7z"] = { icon = "", color = "#76520C", cterm_color = "94", name = "7z" }, + ["Dockerfile"] = { icon = "󰡨", color = "#2E5F99", cterm_color = "25", name = "Dockerfile" }, + ["R"] = { icon = "󰟔", color = "#1A4C8C", cterm_color = "25", name = "R" }, + ["a"] = { icon = "", color = "#494A47", cterm_color = "239", name = "StaticLibraryArchive" }, + ["aac"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "AdvancedAudioCoding" }, + ["ada"] = { icon = "", color = "#3B69AA", cterm_color = "25", name = "AdaFile" }, + ["adb"] = { icon = "", color = "#3B69AA", cterm_color = "25", name = "AdaBody" }, + ["ads"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "AdaSpecification" }, + ["ai"] = { icon = "", color = "#666620", cterm_color = "58", name = "Ai" }, + ["aif"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "AudioInterchangeFileFormat" }, + ["aiff"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "AudioInterchangeFileFormat" }, + ["android"] = { icon = "", color = "#277E3E", cterm_color = "29", name = "Android" }, + ["ape"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "MonkeysAudio" }, + ["apk"] = { icon = "", color = "#277E3E", cterm_color = "29", name = "apk" }, + ["apl"] = { icon = "", color = "#1B7936", cterm_color = "28", name = "APL" }, + ["app"] = { icon = "", color = "#9F0500", cterm_color = "124", name = "App" }, + ["applescript"] = { icon = "", color = "#526064", cterm_color = "59", name = "AppleScript" }, + ["asc"] = { icon = "󰦝", color = "#41525F", cterm_color = "239", name = "Asc" }, + ["asm"] = { icon = "", color = "#006D8E", cterm_color = "24", name = "ASM" }, + ["ass"] = { icon = "󰨖", color = "#805C0A", cterm_color = "94", name = "Ass" }, + ["astro"] = { icon = "", color = "#AA2F4D", cterm_color = "125", name = "Astro" }, + ["avif"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Avif" }, + ["awk"] = { icon = "", color = "#3A4446", cterm_color = "238", name = "Awk" }, + ["azcli"] = { icon = "", color = "#005A9F", cterm_color = "25", name = "AzureCli" }, + ["bak"] = { icon = "󰁯", color = "#526064", cterm_color = "59", name = "Backup" }, + ["bash"] = { icon = "", color = "#447028", cterm_color = "22", name = "Bash" }, + ["bat"] = { icon = "", color = "#40500F", cterm_color = "58", name = "Bat" }, + ["bazel"] = { icon = "", color = "#447028", cterm_color = "22", name = "Bazel" }, + ["bib"] = { icon = "󱉟", color = "#666620", cterm_color = "58", name = "BibTeX" }, + ["bicep"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Bicep" }, + ["bicepparam"] = { icon = "", color = "#6A4D77", cterm_color = "96", name = "BicepParameters" }, + ["bin"] = { icon = "", color = "#9F0500", cterm_color = "124", name = "Bin" }, + ["blade.php"] = { icon = "", color = "#A0372B", cterm_color = "124", name = "Blade" }, + ["blend"] = { icon = "󰂫", color = "#9C4F00", cterm_color = "130", name = "Blender" }, + ["blp"] = { icon = "󰺾", color = "#3A6497", cterm_color = "25", name = "Blueprint" }, + ["bmp"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Bmp" }, + ["bqn"] = { icon = "", color = "#1B7936", cterm_color = "28", name = "APL" }, + ["brep"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "BoundaryRepresentation" }, + ["bz"] = { icon = "", color = "#76520C", cterm_color = "94", name = "Bz" }, + ["bz2"] = { icon = "", color = "#76520C", cterm_color = "94", name = "Bz2" }, + ["bz3"] = { icon = "", color = "#76520C", cterm_color = "94", name = "Bz3" }, + ["bzl"] = { icon = "", color = "#447028", cterm_color = "22", name = "Bzl" }, + ["c"] = { icon = "", color = "#3B69AA", cterm_color = "25", name = "C" }, + ["c++"] = { icon = "", color = "#A23253", cterm_color = "125", name = "CPlusPlus" }, + ["cache"] = { icon = "", color = "#333333", cterm_color = "236", name = "Cache" }, + ["cast"] = { icon = "", color = "#7E4C10", cterm_color = "94", name = "Asciinema" }, + ["cbl"] = { icon = "", color = "#005CA5", cterm_color = "25", name = "Cobol" }, + ["cc"] = { icon = "", color = "#A23253", cterm_color = "125", name = "CPlusPlus" }, + ["ccm"] = { icon = "", color = "#A23253", cterm_color = "125", name = "CPlusPlusModule" }, + ["cfc"] = { icon = "", color = "#017B8C", cterm_color = "30", name = "ColdFusionScript" }, + ["cfg"] = { icon = "", color = "#526064", cterm_color = "59", name = "Configuration" }, + ["cfm"] = { icon = "", color = "#017B8C", cterm_color = "30", name = "ColdFusionTag" }, + ["cjs"] = { icon = "", color = "#666620", cterm_color = "58", name = "Cjs" }, + ["clj"] = { icon = "", color = "#466024", cterm_color = "22", name = "Clojure" }, + ["cljc"] = { icon = "", color = "#466024", cterm_color = "22", name = "ClojureC" }, + ["cljd"] = { icon = "", color = "#36677C", cterm_color = "24", name = "ClojureDart" }, + ["cljs"] = { icon = "", color = "#36677C", cterm_color = "24", name = "ClojureJS" }, + ["cmake"] = { icon = "", color = "#2C2D2F", cterm_color = "236", name = "CMake" }, + ["cob"] = { icon = "", color = "#005CA5", cterm_color = "25", name = "Cobol" }, + ["cobol"] = { icon = "", color = "#005CA5", cterm_color = "25", name = "Cobol" }, + ["coffee"] = { icon = "", color = "#666620", cterm_color = "58", name = "Coffee" }, + ["conda"] = { icon = "", color = "#2D751C", cterm_color = "28", name = "Conda" }, + ["conf"] = { icon = "", color = "#526064", cterm_color = "59", name = "Conf" }, + ["config.ru"] = { icon = "", color = "#701516", cterm_color = "52", name = "ConfigRu" }, + ["cow"] = { icon = "󰆚", color = "#70421B", cterm_color = "94", name = "CowsayFile" }, + ["cp"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Cp" }, + ["cpp"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Cpp" }, + ["cppm"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Cppm" }, + ["cpy"] = { icon = "", color = "#005CA5", cterm_color = "25", name = "Cobol" }, + ["cr"] = { icon = "", color = "#434343", cterm_color = "238", name = "Crystal" }, + ["crdownload"] = { icon = "", color = "#226654", cterm_color = "23", name = "Crdownload" }, + ["cs"] = { icon = "󰌛", color = "#434D04", cterm_color = "58", name = "Cs" }, + ["csh"] = { icon = "", color = "#3A4446", cterm_color = "238", name = "Csh" }, + ["cshtml"] = { icon = "󱦗", color = "#512BD4", cterm_color = "56", name = "RazorPage" }, + ["cson"] = { icon = "", color = "#666620", cterm_color = "58", name = "Cson" }, + ["csproj"] = { icon = "󰪮", color = "#512BD4", cterm_color = "56", name = "CSharpProject" }, + ["css"] = { icon = "", color = "#663399", cterm_color = "91", name = "Css" }, + ["csv"] = { icon = "", color = "#447028", cterm_color = "22", name = "Csv" }, + ["cts"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Cts" }, + ["cu"] = { icon = "", color = "#447028", cterm_color = "22", name = "cuda" }, + ["cue"] = { icon = "󰲹", color = "#764A57", cterm_color = "95", name = "Cue" }, + ["cuh"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "cudah" }, + ["cxx"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Cxx" }, + ["cxxm"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Cxxm" }, + ["d"] = { icon = "", color = "#842B25", cterm_color = "88", name = "D" }, + ["d.ts"] = { icon = "", color = "#6A4C2A", cterm_color = "94", name = "TypeScriptDeclaration" }, + ["dart"] = { icon = "", color = "#03589C", cterm_color = "25", name = "Dart" }, + ["db"] = { icon = "", color = "#494848", cterm_color = "238", name = "Db" }, + ["dconf"] = { icon = "", color = "#333333", cterm_color = "236", name = "Dconf" }, + ["desktop"] = { icon = "", color = "#563D7C", cterm_color = "54", name = "DesktopEntry" }, + ["diff"] = { icon = "", color = "#41535B", cterm_color = "239", name = "Diff" }, + ["dll"] = { icon = "", color = "#4D2C0B", cterm_color = "52", name = "Dll" }, + ["doc"] = { icon = "󰈬", color = "#185ABD", cterm_color = "26", name = "Doc" }, + ["dockerignore"] = { icon = "󰡨", color = "#2E5F99", cterm_color = "25", name = "DockerIgnore" }, + ["docx"] = { icon = "󰈬", color = "#185ABD", cterm_color = "26", name = "Docx" }, + ["dot"] = { icon = "󱁉", color = "#244A6A", cterm_color = "24", name = "Dot" }, + ["download"] = { icon = "", color = "#226654", cterm_color = "23", name = "Download" }, + ["drl"] = { icon = "", color = "#553A3A", cterm_color = "238", name = "Drools" }, + ["dropbox"] = { icon = "", color = "#0049BE", cterm_color = "26", name = "Dropbox" }, + ["dump"] = { icon = "", color = "#494848", cterm_color = "238", name = "Dump" }, + ["dwg"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "AutoCADDwg" }, + ["dxf"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "AutoCADDxf" }, + ["ebook"] = { icon = "", color = "#755836", cterm_color = "94", name = "Ebook" }, + ["ebuild"] = { icon = "", color = "#4C416E", cterm_color = "60", name = "GentooBuild" }, + ["edn"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Edn" }, + ["eex"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Eex" }, + ["ejs"] = { icon = "", color = "#666620", cterm_color = "58", name = "Ejs" }, + ["el"] = { icon = "", color = "#61568E", cterm_color = "60", name = "Elisp" }, + ["elc"] = { icon = "", color = "#61568E", cterm_color = "60", name = "Elisp" }, + ["elf"] = { icon = "", color = "#9F0500", cterm_color = "124", name = "Elf" }, + ["elm"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Elm" }, + ["eln"] = { icon = "", color = "#61568E", cterm_color = "60", name = "Elisp" }, + ["env"] = { icon = "", color = "#32310D", cterm_color = "236", name = "Env" }, + ["eot"] = { icon = "", color = "#2F2F2F", cterm_color = "236", name = "EmbeddedOpenTypeFont" }, + ["epp"] = { icon = "", color = "#80530D", cterm_color = "94", name = "Epp" }, + ["epub"] = { icon = "", color = "#755836", cterm_color = "94", name = "Epub" }, + ["erb"] = { icon = "", color = "#701516", cterm_color = "52", name = "Erb" }, + ["erl"] = { icon = "", color = "#8A2B72", cterm_color = "89", name = "Erl" }, + ["ex"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Ex" }, + ["exe"] = { icon = "", color = "#9F0500", cterm_color = "124", name = "Exe" }, + ["exs"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Exs" }, + ["f#"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Fsharp" }, + ["f3d"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "Fusion360" }, + ["f90"] = { icon = "󱈚", color = "#563B70", cterm_color = "53", name = "Fortran" }, + ["fbx"] = { icon = "󰆧", color = "#5B5B5B", cterm_color = "240", name = "3DObjectFile" }, + ["fcbak"] = { icon = "", color = "#98262C", cterm_color = "88", name = "FreeCAD" }, + ["fcmacro"] = { icon = "", color = "#98262C", cterm_color = "88", name = "FreeCAD" }, + ["fcmat"] = { icon = "", color = "#98262C", cterm_color = "88", name = "FreeCAD" }, + ["fcparam"] = { icon = "", color = "#98262C", cterm_color = "88", name = "FreeCAD" }, + ["fcscript"] = { icon = "", color = "#98262C", cterm_color = "88", name = "FreeCAD" }, + ["fcstd"] = { icon = "", color = "#98262C", cterm_color = "88", name = "FreeCAD" }, + ["fcstd1"] = { icon = "", color = "#98262C", cterm_color = "88", name = "FreeCAD" }, + ["fctb"] = { icon = "", color = "#98262C", cterm_color = "88", name = "FreeCAD" }, + ["fctl"] = { icon = "", color = "#98262C", cterm_color = "88", name = "FreeCAD" }, + ["fdmdownload"] = { icon = "", color = "#226654", cterm_color = "23", name = "Fdmdownload" }, + ["feature"] = { icon = "", color = "#007E12", cterm_color = "28", name = "Feature" }, + ["fish"] = { icon = "", color = "#3A4446", cterm_color = "238", name = "Fish" }, + ["flac"] = { icon = "", color = "#005880", cterm_color = "24", name = "FreeLosslessAudioCodec" }, + ["flc"] = { icon = "", color = "#2F2F2F", cterm_color = "236", name = "FIGletFontControl" }, + ["flf"] = { icon = "", color = "#2F2F2F", cterm_color = "236", name = "FIGletFontFormat" }, + ["fnl"] = { icon = "", color = "#33312B", cterm_color = "236", name = "Fennel" }, + ["fodg"] = { icon = "", color = "#333211", cterm_color = "236", name = "LibreOfficeGraphics" }, + ["fodp"] = { icon = "", color = "#7F4E22", cterm_color = "94", name = "LibreOfficeImpress" }, + ["fods"] = { icon = "", color = "#28541A", cterm_color = "22", name = "LibreOfficeCalc" }, + ["fodt"] = { icon = "", color = "#16667E", cterm_color = "24", name = "LibreOfficeWriter" }, + ["frag"] = { icon = "", color = "#40647C", cterm_color = "24", name = "FragmentShader" }, + ["fs"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Fs" }, + ["fsi"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Fsi" }, + ["fsscript"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Fsscript" }, + ["fsx"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Fsx" }, + ["gcode"] = { icon = "󰐫", color = "#0F5582", cterm_color = "24", name = "GCode" }, + ["gd"] = { icon = "", color = "#526064", cterm_color = "59", name = "GDScript" }, + ["gemspec"] = { icon = "", color = "#701516", cterm_color = "52", name = "Gemspec" }, + ["geom"] = { icon = "", color = "#40647C", cterm_color = "24", name = "GeometryShader" }, + ["gif"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Gif" }, + ["git"] = { icon = "", color = "#B5391E", cterm_color = "160", name = "GitLogo" }, + ["glb"] = { icon = "", color = "#80581E", cterm_color = "94", name = "BinaryGLTF" }, + ["gleam"] = { icon = "", color = "#553A51", cterm_color = "53", name = "Gleam" }, + ["glsl"] = { icon = "", color = "#40647C", cterm_color = "24", name = "OpenGLShadingLanguage" }, + ["gnumakefile"] = { icon = "", color = "#526064", cterm_color = "59", name = "Makefile" }, + ["go"] = { icon = "", color = "#0082A2", cterm_color = "31", name = "Go" }, + ["godot"] = { icon = "", color = "#526064", cterm_color = "59", name = "GodotProject" }, + ["gpr"] = { icon = "", color = "#526064", cterm_color = "59", name = "GPRBuildProject" }, + ["gql"] = { icon = "", color = "#AC2880", cterm_color = "126", name = "GraphQL" }, + ["gradle"] = { icon = "", color = "#005F87", cterm_color = "24", name = "Gradle" }, + ["graphql"] = { icon = "", color = "#AC2880", cterm_color = "126", name = "GraphQL" }, + ["gresource"] = { icon = "", color = "#333333", cterm_color = "236", name = "GTK" }, + ["gv"] = { icon = "󱁉", color = "#244A6A", cterm_color = "24", name = "Gv" }, + ["gz"] = { icon = "", color = "#76520C", cterm_color = "94", name = "Gz" }, + ["h"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "H" }, + ["haml"] = { icon = "", color = "#2F2F2D", cterm_color = "236", name = "Haml" }, + ["hbs"] = { icon = "", color = "#A04F1D", cterm_color = "130", name = "Hbs" }, + ["heex"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Heex" }, + ["hex"] = { icon = "", color = "#224ABF", cterm_color = "26", name = "Hexadecimal" }, + ["hh"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Hh" }, + ["hpp"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Hpp" }, + ["hrl"] = { icon = "", color = "#8A2B72", cterm_color = "89", name = "Hrl" }, + ["hs"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Hs" }, + ["htm"] = { icon = "", color = "#AA391C", cterm_color = "124", name = "Htm" }, + ["html"] = { icon = "", color = "#AB3A1C", cterm_color = "124", name = "Html" }, + ["http"] = { icon = "", color = "#006A95", cterm_color = "24", name = "HTTP" }, + ["huff"] = { icon = "󰡘", color = "#4242C7", cterm_color = "56", name = "Huff" }, + ["hurl"] = { icon = "", color = "#BF0266", cterm_color = "125", name = "Hurl" }, + ["hx"] = { icon = "", color = "#9C5715", cterm_color = "130", name = "Haxe" }, + ["hxx"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Hxx" }, + ["ical"] = { icon = "", color = "#2B2E83", cterm_color = "18", name = "Ical" }, + ["icalendar"] = { icon = "", color = "#2B2E83", cterm_color = "18", name = "Icalendar" }, + ["ico"] = { icon = "", color = "#666620", cterm_color = "58", name = "Ico" }, + ["ics"] = { icon = "", color = "#2B2E83", cterm_color = "18", name = "Ics" }, + ["ifb"] = { icon = "", color = "#2B2E83", cterm_color = "18", name = "Ifb" }, + ["ifc"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "Ifc" }, + ["ige"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "Ige" }, + ["iges"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "Iges" }, + ["igs"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "Igs" }, + ["image"] = { icon = "", color = "#453F43", cterm_color = "238", name = "Image" }, + ["img"] = { icon = "", color = "#453F43", cterm_color = "238", name = "Img" }, + ["import"] = { icon = "", color = "#2F2F2F", cterm_color = "236", name = "ImportConfiguration" }, + ["info"] = { icon = "", color = "#333329", cterm_color = "236", name = "Info" }, + ["ini"] = { icon = "", color = "#526064", cterm_color = "59", name = "Ini" }, + ["ino"] = { icon = "", color = "#397981", cterm_color = "30", name = "Arduino" }, + ["ipynb"] = { icon = "", color = "#A35301", cterm_color = "130", name = "Notebook" }, + ["iso"] = { icon = "", color = "#453F43", cterm_color = "238", name = "Iso" }, + ["ixx"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Ixx" }, + ["jar"] = { icon = "", color = "#805834", cterm_color = "94", name = "Jar" }, + ["java"] = { icon = "", color = "#992E33", cterm_color = "88", name = "Java" }, + ["jl"] = { icon = "", color = "#6C4B7C", cterm_color = "96", name = "Jl" }, + ["jpeg"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Jpeg" }, + ["jpg"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Jpg" }, + ["js"] = { icon = "", color = "#666620", cterm_color = "58", name = "Js" }, + ["json"] = { icon = "", color = "#666620", cterm_color = "58", name = "Json" }, + ["json5"] = { icon = "", color = "#666620", cterm_color = "58", name = "Json5" }, + ["jsonc"] = { icon = "", color = "#666620", cterm_color = "58", name = "Jsonc" }, + ["jsx"] = { icon = "", color = "#158197", cterm_color = "31", name = "Jsx" }, + ["jwmrc"] = { icon = "", color = "#005A9A", cterm_color = "25", name = "JWM" }, + ["jxl"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "JpegXl" }, + ["kbx"] = { icon = "󰯄", color = "#565856", cterm_color = "240", name = "Kbx" }, + ["kdb"] = { icon = "", color = "#3E7427", cterm_color = "28", name = "Kdb" }, + ["kdbx"] = { icon = "", color = "#3E7427", cterm_color = "28", name = "Kdbx" }, + ["kdenlive"] = { icon = "", color = "#425C79", cterm_color = "24", name = "Kdenlive" }, + ["kdenlivetitle"] = { icon = "", color = "#425C79", cterm_color = "24", name = "Kdenlive" }, + ["kicad_dru"] = { icon = "", color = "#333333", cterm_color = "236", name = "KiCad" }, + ["kicad_mod"] = { icon = "", color = "#333333", cterm_color = "236", name = "KiCad" }, + ["kicad_pcb"] = { icon = "", color = "#333333", cterm_color = "236", name = "KiCad" }, + ["kicad_prl"] = { icon = "", color = "#333333", cterm_color = "236", name = "KiCad" }, + ["kicad_pro"] = { icon = "", color = "#333333", cterm_color = "236", name = "KiCad" }, + ["kicad_sch"] = { icon = "", color = "#333333", cterm_color = "236", name = "KiCad" }, + ["kicad_sym"] = { icon = "", color = "#333333", cterm_color = "236", name = "KiCad" }, + ["kicad_wks"] = { icon = "", color = "#333333", cterm_color = "236", name = "KiCad" }, + ["ko"] = { icon = "", color = "#494A47", cterm_color = "239", name = "LinuxKernelObject" }, + ["kpp"] = { icon = "", color = "#A12EA7", cterm_color = "127", name = "Krita" }, + ["kra"] = { icon = "", color = "#A12EA7", cterm_color = "127", name = "Krita" }, + ["krz"] = { icon = "", color = "#A12EA7", cterm_color = "127", name = "Krita" }, + ["ksh"] = { icon = "", color = "#3A4446", cterm_color = "238", name = "Ksh" }, + ["kt"] = { icon = "", color = "#5F3EBF", cterm_color = "92", name = "Kotlin" }, + ["kts"] = { icon = "", color = "#5F3EBF", cterm_color = "92", name = "KotlinScript" }, + ["lck"] = { icon = "", color = "#5E5E5E", cterm_color = "59", name = "Lock" }, + ["leex"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Leex" }, + ["less"] = { icon = "", color = "#563D7C", cterm_color = "54", name = "Less" }, + ["lff"] = { icon = "", color = "#2F2F2F", cterm_color = "236", name = "LibrecadFontFile" }, + ["lhs"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Lhs" }, + ["lib"] = { icon = "", color = "#4D2C0B", cterm_color = "52", name = "Lib" }, + ["license"] = { icon = "", color = "#666620", cterm_color = "58", name = "License" }, + ["liquid"] = { icon = "", color = "#4A6024", cterm_color = "58", name = "Liquid" }, + ["lock"] = { icon = "", color = "#5E5E5E", cterm_color = "59", name = "Lock" }, + ["log"] = { icon = "󰌱", color = "#4A4A4A", cterm_color = "239", name = "Log" }, + ["lrc"] = { icon = "󰨖", color = "#805C0A", cterm_color = "94", name = "Lrc" }, + ["lua"] = { icon = "", color = "#366B8A", cterm_color = "24", name = "Lua" }, + ["luac"] = { icon = "", color = "#366B8A", cterm_color = "24", name = "Lua" }, + ["luau"] = { icon = "", color = "#007ABF", cterm_color = "32", name = "Luau" }, + ["m"] = { icon = "", color = "#3B69AA", cterm_color = "25", name = "ObjectiveC" }, + ["m3u"] = { icon = "󰲹", color = "#764A57", cterm_color = "95", name = "M3u" }, + ["m3u8"] = { icon = "󰲹", color = "#764A57", cterm_color = "95", name = "M3u8" }, + ["m4a"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "MPEG4" }, + ["m4v"] = { icon = "", color = "#7E4C10", cterm_color = "94", name = "M4V" }, + ["magnet"] = { icon = "", color = "#A51B16", cterm_color = "124", name = "Magnet" }, + ["makefile"] = { icon = "", color = "#526064", cterm_color = "59", name = "Makefile" }, + ["markdown"] = { icon = "", color = "#4A4A4A", cterm_color = "239", name = "Markdown" }, + ["material"] = { icon = "", color = "#8A2B72", cterm_color = "89", name = "Material" }, + ["md"] = { icon = "", color = "#4A4A4A", cterm_color = "239", name = "Md" }, + ["md5"] = { icon = "󰕥", color = "#5D5975", cterm_color = "60", name = "Md5" }, + ["mdx"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Mdx" }, + ["mint"] = { icon = "󰌪", color = "#44604A", cterm_color = "23", name = "Mint" }, + ["mjs"] = { icon = "", color = "#504B1E", cterm_color = "58", name = "Mjs" }, + ["mk"] = { icon = "", color = "#526064", cterm_color = "59", name = "Makefile" }, + ["mkv"] = { icon = "", color = "#7E4C10", cterm_color = "94", name = "Mkv" }, + ["ml"] = { icon = "", color = "#975122", cterm_color = "130", name = "Ml" }, + ["mli"] = { icon = "", color = "#975122", cterm_color = "130", name = "Mli" }, + ["mm"] = { icon = "", color = "#36677C", cterm_color = "24", name = "ObjectiveCPlusPlus" }, + ["mo"] = { icon = "", color = "#654CA7", cterm_color = "61", name = "Motoko" }, + ["mobi"] = { icon = "", color = "#755836", cterm_color = "94", name = "Mobi" }, + ["mojo"] = { icon = "", color = "#BF3917", cterm_color = "160", name = "Mojo" }, + ["mov"] = { icon = "", color = "#7E4C10", cterm_color = "94", name = "MOV" }, + ["mp3"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "MPEGAudioLayerIII" }, + ["mp4"] = { icon = "", color = "#7E4C10", cterm_color = "94", name = "Mp4" }, + ["mpp"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Mpp" }, + ["msf"] = { icon = "", color = "#0E5CA9", cterm_color = "25", name = "Thunderbird" }, + ["mts"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Mts" }, + ["mustache"] = { icon = "", color = "#975122", cterm_color = "130", name = "Mustache" }, + ["nfo"] = { icon = "", color = "#333329", cterm_color = "236", name = "Nfo" }, + ["nim"] = { icon = "", color = "#514700", cterm_color = "58", name = "Nim" }, + ["nix"] = { icon = "", color = "#3F5D72", cterm_color = "24", name = "Nix" }, + ["norg"] = { icon = "", color = "#365A8E", cterm_color = "25", name = "Norg" }, + ["nswag"] = { icon = "", color = "#427516", cterm_color = "28", name = "Nswag" }, + ["nu"] = { icon = "", color = "#276F4E", cterm_color = "29", name = "Nushell" }, + ["o"] = { icon = "", color = "#9F0500", cterm_color = "124", name = "ObjectFile" }, + ["obj"] = { icon = "󰆧", color = "#5B5B5B", cterm_color = "240", name = "3DObjectFile" }, + ["odf"] = { icon = "", color = "#AA3C64", cterm_color = "125", name = "LibreOfficeFormula" }, + ["odg"] = { icon = "", color = "#333211", cterm_color = "236", name = "LibreOfficeGraphics" }, + ["odin"] = { icon = "󰟢", color = "#2A629E", cterm_color = "25", name = "Odin" }, + ["odp"] = { icon = "", color = "#7F4E22", cterm_color = "94", name = "LibreOfficeImpress" }, + ["ods"] = { icon = "", color = "#28541A", cterm_color = "22", name = "LibreOfficeCalc" }, + ["odt"] = { icon = "", color = "#16667E", cterm_color = "24", name = "LibreOfficeWriter" }, + ["oga"] = { icon = "", color = "#005880", cterm_color = "24", name = "OggVorbis" }, + ["ogg"] = { icon = "", color = "#005880", cterm_color = "24", name = "OggVorbis" }, + ["ogv"] = { icon = "", color = "#7E4C10", cterm_color = "94", name = "OggVideo" }, + ["ogx"] = { icon = "", color = "#7E4C10", cterm_color = "94", name = "OggMultiplex" }, + ["opus"] = { icon = "", color = "#005880", cterm_color = "24", name = "OpusAudioFile" }, + ["org"] = { icon = "", color = "#4F7166", cterm_color = "66", name = "OrgMode" }, + ["otf"] = { icon = "", color = "#2F2F2F", cterm_color = "236", name = "OpenTypeFont" }, + ["out"] = { icon = "", color = "#9F0500", cterm_color = "124", name = "Out" }, + ["part"] = { icon = "", color = "#226654", cterm_color = "23", name = "Part" }, + ["patch"] = { icon = "", color = "#41535B", cterm_color = "239", name = "Patch" }, + ["pck"] = { icon = "", color = "#526064", cterm_color = "59", name = "PackedResource" }, + ["pcm"] = { icon = "", color = "#005880", cterm_color = "24", name = "PulseCodeModulation" }, + ["pdf"] = { icon = "", color = "#B30B00", cterm_color = "124", name = "Pdf" }, + ["php"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Php" }, + ["pl"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Pl" }, + ["pls"] = { icon = "󰲹", color = "#764A57", cterm_color = "95", name = "Pls" }, + ["ply"] = { icon = "󰆧", color = "#5B5B5B", cterm_color = "240", name = "3DObjectFile" }, + ["pm"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Pm" }, + ["png"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Png" }, + ["po"] = { icon = "", color = "#1C708E", cterm_color = "24", name = "Localization" }, + ["pot"] = { icon = "", color = "#1C708E", cterm_color = "24", name = "Localization" }, + ["pp"] = { icon = "", color = "#80530D", cterm_color = "94", name = "Pp" }, + ["ppt"] = { icon = "󰈧", color = "#983826", cterm_color = "124", name = "Ppt" }, + ["pptx"] = { icon = "󰈧", color = "#983826", cterm_color = "124", name = "Pptx" }, + ["prisma"] = { icon = "", color = "#444DA2", cterm_color = "61", name = "Prisma" }, + ["pro"] = { icon = "", color = "#725C2A", cterm_color = "94", name = "Prolog" }, + ["ps1"] = { icon = "󰨊", color = "#325698", cterm_color = "25", name = "PsScriptfile" }, + ["psb"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Psb" }, + ["psd"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Psd" }, + ["psd1"] = { icon = "󰨊", color = "#4F5893", cterm_color = "60", name = "PsManifestfile" }, + ["psm1"] = { icon = "󰨊", color = "#4F5893", cterm_color = "60", name = "PsScriptModulefile" }, + ["pub"] = { icon = "󰷖", color = "#4C422F", cterm_color = "238", name = "Pub" }, + ["pxd"] = { icon = "", color = "#3C6F98", cterm_color = "24", name = "Pxd" }, + ["pxi"] = { icon = "", color = "#3C6F98", cterm_color = "24", name = "Pxi" }, + ["py"] = { icon = "", color = "#805E02", cterm_color = "94", name = "Py" }, + ["pyc"] = { icon = "", color = "#332D1D", cterm_color = "236", name = "Pyc" }, + ["pyd"] = { icon = "", color = "#332D1D", cterm_color = "236", name = "Pyd" }, + ["pyi"] = { icon = "", color = "#805E02", cterm_color = "94", name = "Pyi" }, + ["pyo"] = { icon = "", color = "#332D1D", cterm_color = "236", name = "Pyo" }, + ["pyw"] = { icon = "", color = "#3C6F98", cterm_color = "24", name = "Pyw" }, + ["pyx"] = { icon = "", color = "#3C6F98", cterm_color = "24", name = "Pyx" }, + ["qm"] = { icon = "", color = "#1C708E", cterm_color = "24", name = "Localization" }, + ["qml"] = { icon = "", color = "#2B8937", cterm_color = "28", name = "Qt" }, + ["qrc"] = { icon = "", color = "#2B8937", cterm_color = "28", name = "Qt" }, + ["qss"] = { icon = "", color = "#2B8937", cterm_color = "28", name = "Qt" }, + ["query"] = { icon = "", color = "#607035", cterm_color = "58", name = "Query" }, + ["r"] = { icon = "󰟔", color = "#1A4C8C", cterm_color = "25", name = "R" }, + ["rake"] = { icon = "", color = "#701516", cterm_color = "52", name = "Rake" }, + ["rar"] = { icon = "", color = "#76520C", cterm_color = "94", name = "Rar" }, + ["rasi"] = { icon = "", color = "#666620", cterm_color = "58", name = "Rasi" }, + ["razor"] = { icon = "󱦘", color = "#512BD4", cterm_color = "56", name = "RazorPage" }, + ["rb"] = { icon = "", color = "#701516", cterm_color = "52", name = "Rb" }, + ["res"] = { icon = "", color = "#992E33", cterm_color = "88", name = "ReScript" }, + ["resi"] = { icon = "", color = "#A33759", cterm_color = "125", name = "ReScriptInterface" }, + ["rlib"] = { icon = "", color = "#6F5242", cterm_color = "95", name = "Rlib" }, + ["rmd"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Rmd" }, + ["rproj"] = { icon = "󰗆", color = "#286844", cterm_color = "29", name = "Rproj" }, + ["rs"] = { icon = "", color = "#6F5242", cterm_color = "95", name = "Rs" }, + ["rss"] = { icon = "", color = "#7E4E1E", cterm_color = "94", name = "Rss" }, + ["s"] = { icon = "", color = "#005594", cterm_color = "25", name = "ASM" }, + ["sass"] = { icon = "", color = "#A33759", cterm_color = "125", name = "Sass" }, + ["sbt"] = { icon = "", color = "#992E33", cterm_color = "88", name = "sbt" }, + ["sc"] = { icon = "", color = "#992E33", cterm_color = "88", name = "ScalaScript" }, + ["scad"] = { icon = "", color = "#53480F", cterm_color = "58", name = "OpenSCAD" }, + ["scala"] = { icon = "", color = "#992E33", cterm_color = "88", name = "Scala" }, + ["scm"] = { icon = "󰘧", color = "#303030", cterm_color = "236", name = "Scheme" }, + ["scss"] = { icon = "", color = "#A33759", cterm_color = "125", name = "Scss" }, + ["sh"] = { icon = "", color = "#3A4446", cterm_color = "238", name = "Sh" }, + ["sha1"] = { icon = "󰕥", color = "#5D5975", cterm_color = "60", name = "Sha1" }, + ["sha224"] = { icon = "󰕥", color = "#5D5975", cterm_color = "60", name = "Sha224" }, + ["sha256"] = { icon = "󰕥", color = "#5D5975", cterm_color = "60", name = "Sha256" }, + ["sha384"] = { icon = "󰕥", color = "#5D5975", cterm_color = "60", name = "Sha384" }, + ["sha512"] = { icon = "󰕥", color = "#5D5975", cterm_color = "60", name = "Sha512" }, + ["sig"] = { icon = "󰘧", color = "#975122", cterm_color = "130", name = "Sig" }, + ["signature"] = { icon = "󰘧", color = "#975122", cterm_color = "130", name = "Signature" }, + ["skp"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "SketchUp" }, + ["sldasm"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "SolidWorksAsm" }, + ["sldprt"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "SolidWorksPrt" }, + ["slim"] = { icon = "", color = "#AA391C", cterm_color = "124", name = "Slim" }, + ["sln"] = { icon = "", color = "#643995", cterm_color = "91", name = "Sln" }, + ["slnx"] = { icon = "", color = "#643995", cterm_color = "91", name = "Slnx" }, + ["slvs"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "SolveSpace" }, + ["sml"] = { icon = "󰘧", color = "#975122", cterm_color = "130", name = "Sml" }, + ["so"] = { icon = "", color = "#494A47", cterm_color = "239", name = "SharedObject" }, + ["sol"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Solidity" }, + ["spec.js"] = { icon = "", color = "#666620", cterm_color = "58", name = "SpecJs" }, + ["spec.jsx"] = { icon = "", color = "#158197", cterm_color = "31", name = "JavaScriptReactSpec" }, + ["spec.ts"] = { icon = "", color = "#36677C", cterm_color = "24", name = "SpecTs" }, + ["spec.tsx"] = { icon = "", color = "#1354BF", cterm_color = "26", name = "TypeScriptReactSpec" }, + ["spx"] = { icon = "", color = "#005880", cterm_color = "24", name = "OggSpeexAudio" }, + ["sql"] = { icon = "", color = "#494848", cterm_color = "238", name = "Sql" }, + ["sqlite"] = { icon = "", color = "#494848", cterm_color = "238", name = "Sql" }, + ["sqlite3"] = { icon = "", color = "#494848", cterm_color = "238", name = "Sql" }, + ["srt"] = { icon = "󰨖", color = "#805C0A", cterm_color = "94", name = "Srt" }, + ["ssa"] = { icon = "󰨖", color = "#805C0A", cterm_color = "94", name = "Ssa" }, + ["ste"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "Ste" }, + ["step"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "Step" }, + ["stl"] = { icon = "󰆧", color = "#5B5B5B", cterm_color = "240", name = "3DObjectFile" }, + ["stories.js"] = { icon = "", color = "#AA2F59", cterm_color = "125", name = "StorybookJavaScript" }, + ["stories.jsx"] = { icon = "", color = "#AA2F59", cterm_color = "125", name = "StorybookJsx" }, + ["stories.mjs"] = { icon = "", color = "#AA2F59", cterm_color = "125", name = "StorybookMjs" }, + ["stories.svelte"] = { icon = "", color = "#AA2F59", cterm_color = "125", name = "StorybookSvelte" }, + ["stories.ts"] = { icon = "", color = "#AA2F59", cterm_color = "125", name = "StorybookTypeScript" }, + ["stories.tsx"] = { icon = "", color = "#AA2F59", cterm_color = "125", name = "StorybookTsx" }, + ["stories.vue"] = { icon = "", color = "#AA2F59", cterm_color = "125", name = "StorybookVue" }, + ["stp"] = { icon = "󰻫", color = "#576342", cterm_color = "58", name = "Stp" }, + ["strings"] = { icon = "", color = "#1C708E", cterm_color = "24", name = "Localization" }, + ["styl"] = { icon = "", color = "#466024", cterm_color = "22", name = "Styl" }, + ["sub"] = { icon = "󰨖", color = "#805C0A", cterm_color = "94", name = "Sub" }, + ["sublime"] = { icon = "", color = "#975122", cterm_color = "130", name = "Sublime" }, + ["suo"] = { icon = "", color = "#643995", cterm_color = "91", name = "Suo" }, + ["sv"] = { icon = "󰍛", color = "#017226", cterm_color = "22", name = "SystemVerilog" }, + ["svelte"] = { icon = "", color = "#BF2E00", cterm_color = "160", name = "Svelte" }, + ["svg"] = { icon = "󰜡", color = "#80581E", cterm_color = "94", name = "Svg" }, + ["svgz"] = { icon = "󰜡", color = "#80581E", cterm_color = "94", name = "Svgz" }, + ["svh"] = { icon = "󰍛", color = "#017226", cterm_color = "22", name = "SystemVerilog" }, + ["swift"] = { icon = "", color = "#975122", cterm_color = "130", name = "Swift" }, + ["t"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Tor" }, + ["tbc"] = { icon = "󰛓", color = "#1E5CB3", cterm_color = "25", name = "Tcl" }, + ["tcl"] = { icon = "󰛓", color = "#1E5CB3", cterm_color = "25", name = "Tcl" }, + ["templ"] = { icon = "", color = "#6E5E18", cterm_color = "58", name = "Templ" }, + ["terminal"] = { icon = "", color = "#217929", cterm_color = "28", name = "Terminal" }, + ["test.js"] = { icon = "", color = "#666620", cterm_color = "58", name = "TestJs" }, + ["test.jsx"] = { icon = "", color = "#158197", cterm_color = "31", name = "JavaScriptReactTest" }, + ["test.ts"] = { icon = "", color = "#36677C", cterm_color = "24", name = "TestTs" }, + ["test.tsx"] = { icon = "", color = "#1354BF", cterm_color = "26", name = "TypeScriptReactTest" }, + ["tex"] = { icon = "", color = "#3D6117", cterm_color = "22", name = "Tex" }, + ["tf"] = { icon = "", color = "#4732AF", cterm_color = "55", name = "Terraform" }, + ["tfvars"] = { icon = "", color = "#4732AF", cterm_color = "55", name = "TFVars" }, + ["tgz"] = { icon = "", color = "#76520C", cterm_color = "94", name = "Tgz" }, + ["tmpl"] = { icon = "", color = "#6E5E18", cterm_color = "58", name = "Template" }, + ["tmux"] = { icon = "", color = "#0F8C13", cterm_color = "28", name = "Tmux" }, + ["toml"] = { icon = "", color = "#753219", cterm_color = "88", name = "Toml" }, + ["torrent"] = { icon = "", color = "#226654", cterm_color = "23", name = "Torrent" }, + ["tres"] = { icon = "", color = "#526064", cterm_color = "59", name = "GodotTextResource" }, + ["ts"] = { icon = "", color = "#36677C", cterm_color = "24", name = "TypeScript" }, + ["tscn"] = { icon = "", color = "#526064", cterm_color = "59", name = "GodotTextScene" }, + ["tsconfig"] = { icon = "", color = "#AA5A00", cterm_color = "130", name = "TypoScriptConfig" }, + ["tsx"] = { icon = "", color = "#1354BF", cterm_color = "26", name = "Tsx" }, + ["ttf"] = { icon = "", color = "#2F2F2F", cterm_color = "236", name = "TrueTypeFont" }, + ["twig"] = { icon = "", color = "#466024", cterm_color = "22", name = "Twig" }, + ["txt"] = { icon = "󰈙", color = "#447028", cterm_color = "22", name = "Txt" }, + ["txz"] = { icon = "", color = "#76520C", cterm_color = "94", name = "Txz" }, + ["typ"] = { icon = "", color = "#097D80", cterm_color = "30", name = "Typst" }, + ["typoscript"] = { icon = "", color = "#AA5A00", cterm_color = "130", name = "TypoScript" }, + ["ui"] = { icon = "", color = "#015BF0", cterm_color = "27", name = "UI" }, + ["v"] = { icon = "󰍛", color = "#017226", cterm_color = "22", name = "Verilog" }, + ["vala"] = { icon = "", color = "#5C2E8B", cterm_color = "54", name = "Vala" }, + ["vert"] = { icon = "", color = "#40647C", cterm_color = "24", name = "VertexShader" }, + ["vh"] = { icon = "󰍛", color = "#017226", cterm_color = "22", name = "Verilog" }, + ["vhd"] = { icon = "󰍛", color = "#017226", cterm_color = "22", name = "VHDL" }, + ["vhdl"] = { icon = "󰍛", color = "#017226", cterm_color = "22", name = "VHDL" }, + ["vi"] = { icon = "", color = "#554203", cterm_color = "58", name = "LabView" }, + ["vim"] = { icon = "", color = "#017226", cterm_color = "22", name = "Vim" }, + ["vsh"] = { icon = "", color = "#3E5A7F", cterm_color = "24", name = "Vlang" }, + ["vsix"] = { icon = "", color = "#643995", cterm_color = "91", name = "Vsix" }, + ["vue"] = { icon = "", color = "#466024", cterm_color = "22", name = "Vue" }, + ["wasm"] = { icon = "", color = "#4539A4", cterm_color = "55", name = "Wasm" }, + ["wav"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "WaveformAudioFile" }, + ["webm"] = { icon = "", color = "#7E4C10", cterm_color = "94", name = "Webm" }, + ["webmanifest"] = { icon = "", color = "#504B1E", cterm_color = "58", name = "Webmanifest" }, + ["webp"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Webp" }, + ["webpack"] = { icon = "󰜫", color = "#36677C", cterm_color = "24", name = "Webpack" }, + ["wma"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "WindowsMediaAudio" }, + ["wmv"] = { icon = "", color = "#7E4C10", cterm_color = "94", name = "WindowsMediaVideo" }, + ["woff"] = { icon = "", color = "#2F2F2F", cterm_color = "236", name = "WebOpenFontFormat" }, + ["woff2"] = { icon = "", color = "#2F2F2F", cterm_color = "236", name = "WebOpenFontFormat" }, + ["wrl"] = { icon = "󰆧", color = "#5B5B5B", cterm_color = "240", name = "VRML" }, + ["wrz"] = { icon = "󰆧", color = "#5B5B5B", cterm_color = "240", name = "VRML" }, + ["wv"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "WavPack" }, + ["wvc"] = { icon = "", color = "#0075AA", cterm_color = "24", name = "WavPackCorrection" }, + ["x"] = { icon = "", color = "#3B69AA", cterm_color = "25", name = "Logos" }, + ["xaml"] = { icon = "󰙳", color = "#512BD4", cterm_color = "56", name = "Xaml" }, + ["xcf"] = { icon = "", color = "#4A4434", cterm_color = "238", name = "GIMP" }, + ["xcplayground"] = { icon = "", color = "#975122", cterm_color = "130", name = "XcPlayground" }, + ["xcstrings"] = { icon = "", color = "#1C708E", cterm_color = "24", name = "XcLocalization" }, + ["xls"] = { icon = "󰈛", color = "#207245", cterm_color = "29", name = "Xls" }, + ["xlsx"] = { icon = "󰈛", color = "#207245", cterm_color = "29", name = "Xlsx" }, + ["xm"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Logos" }, + ["xml"] = { icon = "󰗀", color = "#975122", cterm_color = "130", name = "Xml" }, + ["xpi"] = { icon = "", color = "#BF1401", cterm_color = "124", name = "Xpi" }, + ["xul"] = { icon = "", color = "#975122", cterm_color = "130", name = "Xul" }, + ["xz"] = { icon = "", color = "#76520C", cterm_color = "94", name = "Xz" }, + ["yaml"] = { icon = "", color = "#526064", cterm_color = "59", name = "Yaml" }, + ["yml"] = { icon = "", color = "#526064", cterm_color = "59", name = "Yml" }, + ["zig"] = { icon = "", color = "#7B4D0E", cterm_color = "94", name = "Zig" }, + ["zip"] = { icon = "", color = "#76520C", cterm_color = "94", name = "Zip" }, + ["zsh"] = { icon = "", color = "#447028", cterm_color = "22", name = "Zsh" }, + ["zst"] = { icon = "", color = "#76520C", cterm_color = "94", name = "Zst" }, + ["🔥"] = { icon = "", color = "#BF3917", cterm_color = "160", name = "Mojo" }, +} --[[@as table]] diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_filename.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_filename.lua new file mode 100644 index 0000000..fb4f584 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_filename.lua @@ -0,0 +1,214 @@ +return { -- this file is generated from lua/nvim-web-devicons/default/icons_by_filename.lua, please do not edit + ["vite.config.cjs"] = { icon = "", color = "#805400", cterm_color = "94", name = "ViteConfig" }, + ["vite.config.cts"] = { icon = "", color = "#805400", cterm_color = "94", name = "ViteConfig" }, + ["vite.config.js"] = { icon = "", color = "#805400", cterm_color = "94", name = "ViteConfig" }, + ["vite.config.mjs"] = { icon = "", color = "#805400", cterm_color = "94", name = "ViteConfig" }, + ["vite.config.mts"] = { icon = "", color = "#805400", cterm_color = "94", name = "ViteConfig" }, + ["vite.config.ts"] = { icon = "", color = "#805400", cterm_color = "94", name = "ViteConfig" }, + [".SRCINFO"] = { icon = "󰣇", color = "#0B6F9E", cterm_color = "24", name = "SRCINFO" }, + [".Xauthority"] = { icon = "", color = "#AC3A12", cterm_color = "124", name = "Xauthority" }, + [".Xresources"] = { icon = "", color = "#AC3A12", cterm_color = "124", name = "Xresources" }, + [".babelrc"] = { icon = "", color = "#666620", cterm_color = "58", name = "Babelrc" }, + [".bash_profile"] = { icon = "", color = "#447028", cterm_color = "22", name = "BashProfile" }, + [".bashrc"] = { icon = "", color = "#447028", cterm_color = "22", name = "Bashrc" }, + [".clang-format"] = { icon = "", color = "#526064", cterm_color = "59", name = "ClangConfig" }, + [".clang-tidy"] = { icon = "", color = "#526064", cterm_color = "59", name = "ClangConfig" }, + [".codespellrc"] = { icon = "󰓆", color = "#239140", cterm_color = "28", name = "Codespell" }, + [".condarc"] = { icon = "", color = "#2D751C", cterm_color = "28", name = "Conda" }, + [".dockerignore"] = { icon = "󰡨", color = "#2E5F99", cterm_color = "25", name = "Dockerfile" }, + [".ds_store"] = { icon = "", color = "#41535B", cterm_color = "239", name = "DsStore" }, + [".editorconfig"] = { icon = "", color = "#333030", cterm_color = "236", name = "EditorConfig" }, + [".env"] = { icon = "", color = "#32310D", cterm_color = "236", name = "Env" }, + [".eslintignore"] = { icon = "", color = "#4B32C3", cterm_color = "56", name = "EslintIgnore" }, + [".eslintrc"] = { icon = "", color = "#4B32C3", cterm_color = "56", name = "Eslintrc" }, + [".git-blame-ignore-revs"] = { icon = "", color = "#B83A1D", cterm_color = "160", name = "GitBlameIgnore" }, + [".gitattributes"] = { icon = "", color = "#B83A1D", cterm_color = "160", name = "GitAttributes" }, + [".gitconfig"] = { icon = "", color = "#B83A1D", cterm_color = "160", name = "GitConfig" }, + [".gitignore"] = { icon = "", color = "#B83A1D", cterm_color = "160", name = "GitIgnore" }, + [".gitlab-ci.yml"] = { icon = "", color = "#AA321F", cterm_color = "124", name = "GitlabCI" }, + [".gitmodules"] = { icon = "", color = "#B83A1D", cterm_color = "160", name = "GitModules" }, + [".gtkrc-2.0"] = { icon = "", color = "#333333", cterm_color = "236", name = "GTK" }, + [".gvimrc"] = { icon = "", color = "#017226", cterm_color = "22", name = "Gvimrc" }, + [".justfile"] = { icon = "", color = "#526064", cterm_color = "59", name = "Justfile" }, + [".luacheckrc"] = { icon = "", color = "#007ABF", cterm_color = "32", name = "Luaurc" }, + [".luaurc"] = { icon = "", color = "#007ABF", cterm_color = "32", name = "Luaurc" }, + [".mailmap"] = { icon = "󰊢", color = "#B83A1D", cterm_color = "160", name = "Mailmap" }, + [".nanorc"] = { icon = "", color = "#440077", cterm_color = "54", name = "Nano" }, + [".npmignore"] = { icon = "", color = "#AE1D38", cterm_color = "161", name = "NPMIgnore" }, + [".npmrc"] = { icon = "", color = "#AE1D38", cterm_color = "161", name = "NPMrc" }, + [".nuxtrc"] = { icon = "󱄆", color = "#00835F", cterm_color = "29", name = "NuxtConfig" }, + [".nvmrc"] = { icon = "", color = "#3F6B34", cterm_color = "22", name = "node" }, + [".pnpmfile.cjs"] = { icon = "", color = "#7C5601", cterm_color = "94", name = "PNPMFile" }, + [".pre-commit-config.yaml"] = { icon = "󰛢", color = "#7C5A12", cterm_color = "94", name = "PreCommitConfig" }, + [".prettierignore"] = { icon = "", color = "#3264B7", cterm_color = "25", name = "PrettierIgnore" }, + [".prettierrc"] = { icon = "", color = "#3264B7", cterm_color = "25", name = "PrettierConfig" }, + [".prettierrc.cjs"] = { icon = "", color = "#3264B7", cterm_color = "25", name = "PrettierConfig" }, + [".prettierrc.js"] = { icon = "", color = "#3264B7", cterm_color = "25", name = "PrettierConfig" }, + [".prettierrc.json"] = { icon = "", color = "#3264B7", cterm_color = "25", name = "PrettierConfig" }, + [".prettierrc.json5"] = { icon = "", color = "#3264B7", cterm_color = "25", name = "PrettierConfig" }, + [".prettierrc.mjs"] = { icon = "", color = "#3264B7", cterm_color = "25", name = "PrettierConfig" }, + [".prettierrc.toml"] = { icon = "", color = "#3264B7", cterm_color = "25", name = "PrettierConfig" }, + [".prettierrc.yaml"] = { icon = "", color = "#3264B7", cterm_color = "25", name = "PrettierConfig" }, + [".prettierrc.yml"] = { icon = "", color = "#3264B7", cterm_color = "25", name = "PrettierConfig" }, + [".pylintrc"] = { icon = "", color = "#526064", cterm_color = "59", name = "PyLintConfig" }, + [".settings.json"] = { icon = "", color = "#643995", cterm_color = "91", name = "SettingsJson" }, + [".vimrc"] = { icon = "", color = "#017226", cterm_color = "22", name = "Vimrc" }, + [".xinitrc"] = { icon = "", color = "#AC3A12", cterm_color = "124", name = "XInitrc" }, + [".xsession"] = { icon = "", color = "#AC3A12", cterm_color = "124", name = "Xsession" }, + [".zprofile"] = { icon = "", color = "#447028", cterm_color = "22", name = "Zshprofile" }, + [".zshenv"] = { icon = "", color = "#447028", cterm_color = "22", name = "Zshenv" }, + [".zshrc"] = { icon = "", color = "#447028", cterm_color = "22", name = "Zshrc" }, + ["AUTHORS"] = { icon = "", color = "#6B4CAA", cterm_color = "61", name = "AUTHORS" }, + ["AUTHORS.txt"] = { icon = "", color = "#6B4CAA", cterm_color = "61", name = "AUTHORS" }, + ["Directory.Build.props"] = { icon = "", color = "#007ABF", cterm_color = "32", name = "BuildProps" }, + ["Directory.Build.targets"] = { icon = "", color = "#007ABF", cterm_color = "32", name = "BuildTargets" }, + ["Directory.Packages.props"] = { icon = "", color = "#007ABF", cterm_color = "32", name = "PackagesProps" }, + ["FreeCAD.conf"] = { icon = "", color = "#98262C", cterm_color = "88", name = "FreeCADConfig" }, + ["Gemfile"] = { icon = "", color = "#701516", cterm_color = "52", name = "Gemfile" }, + ["Jenkinsfile"] = { icon = "", color = "#9E2A26", cterm_color = "124", name = "Jenkins" }, + ["PKGBUILD"] = { icon = "", color = "#0B6F9E", cterm_color = "24", name = "PKGBUILD" }, + ["PrusaSlicer.ini"] = { icon = "", color = "#9D4717", cterm_color = "130", name = "PrusaSlicer" }, + ["PrusaSlicerGcodeViewer.ini"] = { icon = "", color = "#9D4717", cterm_color = "130", name = "PrusaSlicer" }, + ["QtProject.conf"] = { icon = "", color = "#2B8937", cterm_color = "28", name = "Qt" }, + ["_gvimrc"] = { icon = "", color = "#017226", cterm_color = "22", name = "Gvimrc" }, + ["_vimrc"] = { icon = "", color = "#017226", cterm_color = "22", name = "Vimrc" }, + ["brewfile"] = { icon = "", color = "#701516", cterm_color = "52", name = "Brewfile" }, + ["bspwmrc"] = { icon = "", color = "#2F2F2F", cterm_color = "236", name = "BSPWM" }, + ["build"] = { icon = "", color = "#447028", cterm_color = "22", name = "BazelBuild" }, + ["build.gradle"] = { icon = "", color = "#005F87", cterm_color = "24", name = "GradleBuildScript" }, + ["build.zig.zon"] = { icon = "", color = "#7B4D0E", cterm_color = "94", name = "ZigObjectNotation" }, + ["bun.lock"] = { icon = "", color = "#4E4946", cterm_color = "239", name = "BunLockfile" }, + ["bun.lockb"] = { icon = "", color = "#4E4946", cterm_color = "239", name = "BunLockfile" }, + ["cantorrc"] = { icon = "", color = "#1573B6", cterm_color = "32", name = "Cantorrc" }, + ["checkhealth"] = { icon = "󰓙", color = "#3A5A7E", cterm_color = "24", name = "Checkhealth" }, + ["cmakelists.txt"] = { icon = "", color = "#2C2D2F", cterm_color = "236", name = "CMakeLists" }, + ["code_of_conduct"] = { icon = "", color = "#AB104A", cterm_color = "125", name = "CodeOfConduct" }, + ["code_of_conduct.md"] = { icon = "", color = "#AB104A", cterm_color = "125", name = "CodeOfConduct" }, + ["commit_editmsg"] = { icon = "", color = "#B83A1D", cterm_color = "160", name = "GitCommit" }, + ["commitlint.config.js"] = { icon = "󰜘", color = "#207067", cterm_color = "23", name = "CommitlintConfig" }, + ["commitlint.config.ts"] = { icon = "󰜘", color = "#207067", cterm_color = "23", name = "CommitlintConfig" }, + ["compose.yaml"] = { icon = "󰡨", color = "#2E5F99", cterm_color = "25", name = "Dockerfile" }, + ["compose.yml"] = { icon = "󰡨", color = "#2E5F99", cterm_color = "25", name = "Dockerfile" }, + ["config"] = { icon = "", color = "#526064", cterm_color = "59", name = "Config" }, + ["containerfile"] = { icon = "󰡨", color = "#2E5F99", cterm_color = "25", name = "Dockerfile" }, + ["copying"] = { icon = "", color = "#666620", cterm_color = "58", name = "License" }, + ["copying.lesser"] = { icon = "", color = "#666620", cterm_color = "58", name = "License" }, + ["docker-compose.yaml"] = { icon = "󰡨", color = "#2E5F99", cterm_color = "25", name = "Dockerfile" }, + ["docker-compose.yml"] = { icon = "󰡨", color = "#2E5F99", cterm_color = "25", name = "Dockerfile" }, + ["dockerfile"] = { icon = "󰡨", color = "#2E5F99", cterm_color = "25", name = "Dockerfile" }, + ["eslint.config.cjs"] = { icon = "", color = "#4B32C3", cterm_color = "56", name = "Eslintrc" }, + ["eslint.config.js"] = { icon = "", color = "#4B32C3", cterm_color = "56", name = "Eslintrc" }, + ["eslint.config.mjs"] = { icon = "", color = "#4B32C3", cterm_color = "56", name = "Eslintrc" }, + ["eslint.config.ts"] = { icon = "", color = "#4B32C3", cterm_color = "56", name = "Eslintrc" }, + ["ext_typoscript_setup.txt"] = { icon = "", color = "#AA5A00", cterm_color = "130", name = "TypoScriptSetup" }, + ["favicon.ico"] = { icon = "", color = "#666620", cterm_color = "58", name = "Favicon" }, + ["fp-info-cache"] = { icon = "", color = "#333333", cterm_color = "236", name = "KiCadCache" }, + ["fp-lib-table"] = { icon = "", color = "#333333", cterm_color = "236", name = "KiCadFootprintTable" }, + ["gnumakefile"] = { icon = "", color = "#526064", cterm_color = "59", name = "Makefile" }, + ["go.mod"] = { icon = "", color = "#0082A2", cterm_color = "31", name = "GoMod" }, + ["go.sum"] = { icon = "", color = "#0082A2", cterm_color = "31", name = "GoSum" }, + ["go.work"] = { icon = "", color = "#0082A2", cterm_color = "31", name = "GoWork" }, + ["gradle-wrapper.properties"] = { icon = "", color = "#005F87", cterm_color = "24", name = "GradleWrapperProperties" }, + ["gradle.properties"] = { icon = "", color = "#005F87", cterm_color = "24", name = "GradleProperties" }, + ["gradlew"] = { icon = "", color = "#005F87", cterm_color = "24", name = "GradleWrapperScript" }, + ["groovy"] = { icon = "", color = "#384E5D", cterm_color = "239", name = "Groovy" }, + ["gruntfile.babel.js"] = { icon = "", color = "#975122", cterm_color = "130", name = "Gruntfile" }, + ["gruntfile.coffee"] = { icon = "", color = "#975122", cterm_color = "130", name = "Gruntfile" }, + ["gruntfile.js"] = { icon = "", color = "#975122", cterm_color = "130", name = "Gruntfile" }, + ["gruntfile.ts"] = { icon = "", color = "#975122", cterm_color = "130", name = "Gruntfile" }, + ["gtkrc"] = { icon = "", color = "#333333", cterm_color = "236", name = "GTK" }, + ["gulpfile.babel.js"] = { icon = "", color = "#992E33", cterm_color = "88", name = "Gulpfile" }, + ["gulpfile.coffee"] = { icon = "", color = "#992E33", cterm_color = "88", name = "Gulpfile" }, + ["gulpfile.js"] = { icon = "", color = "#992E33", cterm_color = "88", name = "Gulpfile" }, + ["gulpfile.ts"] = { icon = "", color = "#992E33", cterm_color = "88", name = "Gulpfile" }, + ["hypridle.conf"] = { icon = "", color = "#008082", cterm_color = "30", name = "Hypridle" }, + ["hyprland.conf"] = { icon = "", color = "#008082", cterm_color = "30", name = "Hyprland" }, + ["hyprlandd.conf"] = { icon = "", color = "#008082", cterm_color = "30", name = "Hyprlandd" }, + ["hyprlock.conf"] = { icon = "", color = "#008082", cterm_color = "30", name = "Hyprlock" }, + ["hyprpaper.conf"] = { icon = "", color = "#008082", cterm_color = "30", name = "Hyprpaper" }, + ["hyprsunset.conf"] = { icon = "", color = "#008082", cterm_color = "30", name = "Hyprsunset" }, + ["i18n.config.js"] = { icon = "󰗊", color = "#515987", cterm_color = "60", name = "I18nConfig" }, + ["i18n.config.ts"] = { icon = "󰗊", color = "#515987", cterm_color = "60", name = "I18nConfig" }, + ["i3blocks.conf"] = { icon = "", color = "#2E2F30", cterm_color = "236", name = "i3" }, + ["i3status.conf"] = { icon = "", color = "#2E2F30", cterm_color = "236", name = "i3" }, + ["index.theme"] = { icon = "", color = "#1E7B4A", cterm_color = "29", name = "IndexTheme" }, + ["ionic.config.json"] = { icon = "", color = "#355FA5", cterm_color = "25", name = "Ionic" }, + ["justfile"] = { icon = "", color = "#526064", cterm_color = "59", name = "Justfile" }, + ["kalgebrarc"] = { icon = "", color = "#1573B6", cterm_color = "32", name = "Kalgebrarc" }, + ["kdeglobals"] = { icon = "", color = "#1573B6", cterm_color = "32", name = "KDEglobals" }, + ["kdenlive-layoutsrc"] = { icon = "", color = "#425C79", cterm_color = "24", name = "KdenliveLayoutsrc" }, + ["kdenliverc"] = { icon = "", color = "#425C79", cterm_color = "24", name = "Kdenliverc" }, + ["kritadisplayrc"] = { icon = "", color = "#A12EA7", cterm_color = "127", name = "Kritadisplayrc" }, + ["kritarc"] = { icon = "", color = "#A12EA7", cterm_color = "127", name = "Kritarc" }, + ["license"] = { icon = "", color = "#686020", cterm_color = "58", name = "License" }, + ["license.md"] = { icon = "", color = "#686020", cterm_color = "58", name = "License" }, + ["lxde-rc.xml"] = { icon = "", color = "#606060", cterm_color = "59", name = "LXDEConfigFile" }, + ["lxqt.conf"] = { icon = "", color = "#016E9E", cterm_color = "24", name = "LXQtConfigFile" }, + ["makefile"] = { icon = "", color = "#526064", cterm_color = "59", name = "Makefile" }, + ["mix.lock"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "MixLock" }, + ["mpv.conf"] = { icon = "", color = "#3B1342", cterm_color = "53", name = "Mpv" }, + ["next.config.cjs"] = { icon = "", color = "#333333", cterm_color = "236", name = "NextConfigCjs" }, + ["next.config.js"] = { icon = "", color = "#333333", cterm_color = "236", name = "NextConfigJs" }, + ["next.config.ts"] = { icon = "", color = "#333333", cterm_color = "236", name = "NextConfigTs" }, + ["node_modules"] = { icon = "", color = "#AE1D38", cterm_color = "161", name = "NodeModules" }, + ["nuxt.config.cjs"] = { icon = "󱄆", color = "#00835F", cterm_color = "29", name = "NuxtConfig" }, + ["nuxt.config.js"] = { icon = "󱄆", color = "#00835F", cterm_color = "29", name = "NuxtConfig" }, + ["nuxt.config.mjs"] = { icon = "󱄆", color = "#00835F", cterm_color = "29", name = "NuxtConfig" }, + ["nuxt.config.ts"] = { icon = "󱄆", color = "#00835F", cterm_color = "29", name = "NuxtConfig" }, + ["package-lock.json"] = { icon = "", color = "#7A0D21", cterm_color = "52", name = "PackageLockJson" }, + ["package.json"] = { icon = "", color = "#AE1D38", cterm_color = "161", name = "PackageJson" }, + ["platformio.ini"] = { icon = "", color = "#A4571D", cterm_color = "130", name = "Platformio" }, + ["playwright.config.cjs"] = { icon = "", color = "#238226", cterm_color = "28", name = "PlaywrightConfig" }, + ["playwright.config.cts"] = { icon = "", color = "#238226", cterm_color = "28", name = "PlaywrightConfig" }, + ["playwright.config.js"] = { icon = "", color = "#238226", cterm_color = "28", name = "PlaywrightConfig" }, + ["playwright.config.mjs"] = { icon = "", color = "#238226", cterm_color = "28", name = "PlaywrightConfig" }, + ["playwright.config.mts"] = { icon = "", color = "#238226", cterm_color = "28", name = "PlaywrightConfig" }, + ["playwright.config.ts"] = { icon = "", color = "#238226", cterm_color = "28", name = "PlaywrightConfig" }, + ["pnpm-lock.yaml"] = { icon = "", color = "#7C5601", cterm_color = "94", name = "PNPMLock" }, + ["pnpm-workspace.yaml"] = { icon = "", color = "#7C5601", cterm_color = "94", name = "PNPMWorkspace" }, + ["pom.xml"] = { icon = "", color = "#7A0D21", cterm_color = "52", name = "Maven" }, + ["prettier.config.cjs"] = { icon = "", color = "#3264B7", cterm_color = "25", name = "PrettierConfig" }, + ["prettier.config.js"] = { icon = "", color = "#3264B7", cterm_color = "25", name = "PrettierConfig" }, + ["prettier.config.mjs"] = { icon = "", color = "#3264B7", cterm_color = "25", name = "PrettierConfig" }, + ["prettier.config.ts"] = { icon = "", color = "#3264B7", cterm_color = "25", name = "PrettierConfig" }, + ["procfile"] = { icon = "", color = "#6B4D83", cterm_color = "96", name = "Procfile" }, + ["py.typed"] = { icon = "", color = "#805E02", cterm_color = "94", name = "Py.typed" }, + ["rakefile"] = { icon = "", color = "#701516", cterm_color = "52", name = "Rakefile" }, + ["readme"] = { icon = "󰂺", color = "#2F2F2F", cterm_color = "236", name = "Readme" }, + ["readme.md"] = { icon = "󰂺", color = "#2F2F2F", cterm_color = "236", name = "Readme" }, + ["rmd"] = { icon = "", color = "#36677C", cterm_color = "24", name = "Rmd" }, + ["robots.txt"] = { icon = "󰚩", color = "#465470", cterm_color = "60", name = "RobotsTxt" }, + ["security"] = { icon = "󰒃", color = "#3F4143", cterm_color = "238", name = "Security" }, + ["security.md"] = { icon = "󰒃", color = "#3F4143", cterm_color = "238", name = "Security" }, + ["settings.gradle"] = { icon = "", color = "#005F87", cterm_color = "24", name = "GradleSettings" }, + ["svelte.config.js"] = { icon = "", color = "#BF2E00", cterm_color = "160", name = "SvelteConfig" }, + ["sxhkdrc"] = { icon = "", color = "#2F2F2F", cterm_color = "236", name = "BSPWM" }, + ["sym-lib-table"] = { icon = "", color = "#333333", cterm_color = "236", name = "KiCadSymbolTable" }, + ["tailwind.config.js"] = { icon = "󱏿", color = "#158197", cterm_color = "31", name = "TailwindConfig" }, + ["tailwind.config.mjs"] = { icon = "󱏿", color = "#158197", cterm_color = "31", name = "TailwindConfig" }, + ["tailwind.config.ts"] = { icon = "󱏿", color = "#158197", cterm_color = "31", name = "TailwindConfig" }, + ["tmux.conf"] = { icon = "", color = "#0F8C13", cterm_color = "28", name = "Tmux" }, + ["tmux.conf.local"] = { icon = "", color = "#0F8C13", cterm_color = "28", name = "Tmux" }, + ["tsconfig.json"] = { icon = "", color = "#36677C", cterm_color = "24", name = "TSConfig" }, + ["unlicense"] = { icon = "", color = "#686020", cterm_color = "58", name = "License" }, + ["vagrantfile"] = { icon = "", color = "#104ABF", cterm_color = "26", name = "Vagrantfile" }, + ["vercel.json"] = { icon = "", color = "#333333", cterm_color = "236", name = "Vercel" }, + ["vitest.config.cjs"] = { icon = "", color = "#4D6712", cterm_color = "58", name = "VitestConfig" }, + ["vitest.config.cts"] = { icon = "", color = "#4D6712", cterm_color = "58", name = "VitestConfig" }, + ["vitest.config.js"] = { icon = "", color = "#4D6712", cterm_color = "58", name = "VitestConfig" }, + ["vitest.config.mjs"] = { icon = "", color = "#4D6712", cterm_color = "58", name = "VitestConfig" }, + ["vitest.config.mts"] = { icon = "", color = "#4D6712", cterm_color = "58", name = "VitestConfig" }, + ["vitest.config.ts"] = { icon = "", color = "#4D6712", cterm_color = "58", name = "VitestConfig" }, + ["vlcrc"] = { icon = "󰕼", color = "#9F5100", cterm_color = "130", name = "VLC" }, + ["webpack"] = { icon = "󰜫", color = "#36677C", cterm_color = "24", name = "Webpack" }, + ["weston.ini"] = { icon = "", color = "#805E00", cterm_color = "94", name = "Weston" }, + ["workspace"] = { icon = "", color = "#447028", cterm_color = "22", name = "BazelWorkspace" }, + ["wrangler.jsonc"] = { icon = "", color = "#A35615", cterm_color = "130", name = "WranglerConfig" }, + ["wrangler.toml"] = { icon = "", color = "#A35615", cterm_color = "130", name = "WranglerConfig" }, + ["xdph.conf"] = { icon = "", color = "#008082", cterm_color = "30", name = "XDPH" }, + ["xmobarrc"] = { icon = "", color = "#A9333E", cterm_color = "131", name = "xmonad" }, + ["xmobarrc.hs"] = { icon = "", color = "#A9333E", cterm_color = "131", name = "xmonad" }, + ["xmonad.hs"] = { icon = "", color = "#A9333E", cterm_color = "131", name = "xmonad" }, + ["xorg.conf"] = { icon = "", color = "#AC3A12", cterm_color = "124", name = "XorgConf" }, + ["xsettingsd.conf"] = { icon = "", color = "#AC3A12", cterm_color = "124", name = "XSettingsdConf" }, +} --[[@as table]] diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_operating_system.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_operating_system.lua new file mode 100644 index 0000000..cb0cee8 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_operating_system.lua @@ -0,0 +1,62 @@ +return { -- this file is generated from lua/nvim-web-devicons/default/icons_by_operating_system.lua, please do not edit + ["alma"] = { icon = "", color = "#BF3437", cterm_color = "160", name = "Almalinux" }, + ["alpine"] = { icon = "", color = "#0D597F", cterm_color = "24", name = "Alpine" }, + ["aosc"] = { icon = "", color = "#C00000", cterm_color = "124", name = "AOSC" }, + ["apple"] = { icon = "", color = "#515556", cterm_color = "240", name = "Apple" }, + ["arch"] = { icon = "󰣇", color = "#0B6F9E", cterm_color = "24", name = "Arch" }, + ["archcraft"] = { icon = "", color = "#435E52", cterm_color = "23", name = "Archcraft" }, + ["archlabs"] = { icon = "", color = "#503F42", cterm_color = "238", name = "Archlabs" }, + ["arcolinux"] = { icon = "", color = "#44609D", cterm_color = "25", name = "ArcoLinux" }, + ["artix"] = { icon = "", color = "#2B788F", cterm_color = "31", name = "Artix" }, + ["biglinux"] = { icon = "", color = "#127796", cterm_color = "31", name = "BigLinux" }, + ["centos"] = { icon = "", color = "#7A3D6A", cterm_color = "89", name = "Centos" }, + ["crystallinux"] = { icon = "", color = "#A900FF", cterm_color = "129", name = "CrystalLinux" }, + ["debian"] = { icon = "", color = "#A80030", cterm_color = "88", name = "Debian" }, + ["deepin"] = { icon = "", color = "#1D6FA5", cterm_color = "24", name = "Deepin" }, + ["devuan"] = { icon = "", color = "#404A52", cterm_color = "238", name = "Devuan" }, + ["elementary"] = { icon = "", color = "#3B6081", cterm_color = "24", name = "Elementary" }, + ["endeavour"] = { icon = "", color = "#5C2E8B", cterm_color = "54", name = "Endeavour" }, + ["fedora"] = { icon = "", color = "#072A5E", cterm_color = "17", name = "Fedora" }, + ["freebsd"] = { icon = "", color = "#C90F02", cterm_color = "160", name = "FreeBSD" }, + ["garuda"] = { icon = "", color = "#1F57A9", cterm_color = "25", name = "GarudaLinux" }, + ["gentoo"] = { icon = "󰣨", color = "#585667", cterm_color = "60", name = "Gentoo" }, + ["guix"] = { icon = "", color = "#554400", cterm_color = "58", name = "Guix" }, + ["hyperbola"] = { icon = "", color = "#404040", cterm_color = "238", name = "HyperbolaGNULinuxLibre" }, + ["illumos"] = { icon = "", color = "#BF320B", cterm_color = "160", name = "Illumos" }, + ["kali"] = { icon = "", color = "#1D59BF", cterm_color = "26", name = "Kali" }, + ["kdeneon"] = { icon = "", color = "#187C7B", cterm_color = "30", name = "KDEneon" }, + ["kubuntu"] = { icon = "", color = "#005C92", cterm_color = "24", name = "Kubuntu" }, + ["leap"] = { icon = "", color = "#54421F", cterm_color = "58", name = "Leap" }, + ["linux"] = { icon = "", color = "#333332", cterm_color = "236", name = "Linux" }, + ["locos"] = { icon = "", color = "#7D5A01", cterm_color = "94", name = "LocOS" }, + ["lxle"] = { icon = "", color = "#474747", cterm_color = "238", name = "LXLE" }, + ["mageia"] = { icon = "", color = "#1A719F", cterm_color = "24", name = "Mageia" }, + ["manjaro"] = { icon = "", color = "#227B3B", cterm_color = "29", name = "Manjaro" }, + ["mint"] = { icon = "󰣭", color = "#447529", cterm_color = "28", name = "Mint" }, + ["mxlinux"] = { icon = "", color = "#333333", cterm_color = "236", name = "MXLinux" }, + ["nixos"] = { icon = "", color = "#3D586E", cterm_color = "24", name = "NixOS" }, + ["nobara"] = { icon = "", color = "#333333", cterm_color = "236", name = "NobaraLinux" }, + ["openbsd"] = { icon = "", color = "#514310", cterm_color = "58", name = "OpenBSD" }, + ["opensuse"] = { icon = "", color = "#4A7818", cterm_color = "64", name = "openSUSE" }, + ["parabola"] = { icon = "", color = "#515373", cterm_color = "60", name = "ParabolaGNULinuxLibre" }, + ["parrot"] = { icon = "", color = "#2A6F80", cterm_color = "23", name = "Parrot" }, + ["pop_os"] = { icon = "", color = "#307B85", cterm_color = "30", name = "Pop_OS" }, + ["postmarketos"] = { icon = "", color = "#007300", cterm_color = "22", name = "postmarketOS" }, + ["puppylinux"] = { icon = "", color = "#51575C", cterm_color = "240", name = "PuppyLinux" }, + ["qubesos"] = { icon = "", color = "#2957A2", cterm_color = "25", name = "QubesOS" }, + ["raspberry_pi"] = { icon = "", color = "#BE1848", cterm_color = "161", name = "RaspberryPiOS" }, + ["redhat"] = { icon = "󱄛", color = "#EE0000", cterm_color = "196", name = "Redhat" }, + ["rocky"] = { icon = "", color = "#0B865E", cterm_color = "29", name = "RockyLinux" }, + ["sabayon"] = { icon = "", color = "#424242", cterm_color = "238", name = "Sabayon" }, + ["slackware"] = { icon = "", color = "#35477F", cterm_color = "25", name = "Slackware" }, + ["solus"] = { icon = "", color = "#4B5163", cterm_color = "239", name = "Solus" }, + ["tails"] = { icon = "", color = "#56347C", cterm_color = "54", name = "Tails" }, + ["trisquel"] = { icon = "", color = "#0F58B6", cterm_color = "25", name = "TrisquelGNULinux" }, + ["tumbleweed"] = { icon = "", color = "#237B72", cterm_color = "30", name = "Tumbleweed" }, + ["ubuntu"] = { icon = "", color = "#A6360F", cterm_color = "124", name = "Ubuntu" }, + ["vanillaos"] = { icon = "", color = "#533F1A", cterm_color = "58", name = "VanillaOS" }, + ["void"] = { icon = "", color = "#295340", cterm_color = "23", name = "Void" }, + ["windows"] = { icon = "", color = "#007BB3", cterm_color = "67", name = "Windows" }, + ["xerolinux"] = { icon = "", color = "#5B5F97", cterm_color = "60", name = "XeroLinux" }, + ["zorin"] = { icon = "", color = "#0F79AE", cterm_color = "67", name = "Zorin" }, +} --[[@as table]] diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_window_manager.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_window_manager.lua new file mode 100644 index 0000000..30232d5 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/lua/nvim-web-devicons/light/icons_by_window_manager.lua @@ -0,0 +1,14 @@ +return { -- this file is generated from lua/nvim-web-devicons/default/icons_by_window_manager.lua, please do not edit + ["awesomewm"] = { icon = "", color = "#3E4651", cterm_color = "238", name = "awesome" }, + ["bspwm"] = { icon = "", color = "#4F4F4F", cterm_color = "239", name = "BSPWM" }, + ["dwm"] = { icon = "", color = "#0D5980", cterm_color = "24", name = "dwm" }, + ["enlightenment"] = { icon = "", color = "#333333", cterm_color = "236", name = "Enlightenment" }, + ["fluxbox"] = { icon = "", color = "#404040", cterm_color = "238", name = "Fluxbox" }, + ["hyprland"] = { icon = "", color = "#008082", cterm_color = "30", name = "Hyprland" }, + ["i3"] = { icon = "", color = "#2E2F30", cterm_color = "236", name = "i3" }, + ["jwm"] = { icon = "", color = "#005A9A", cterm_color = "25", name = "JWM" }, + ["qtile"] = { icon = "", color = "#333333", cterm_color = "236", name = "Qtile" }, + ["river"] = { icon = "", color = "#000000", cterm_color = "16", name = "river" }, + ["sway"] = { icon = "", color = "#4E5815", cterm_color = "58", name = "Sway" }, + ["xmonad"] = { icon = "", color = "#A9333E", cterm_color = "131", name = "xmonad" }, +} --[[@as table]] diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/plugin/nvim-web-devicons.vim b/.config/nvim/pack/tree/start/nvim-web-devicons/plugin/nvim-web-devicons.vim new file mode 100644 index 0000000..beccf9e --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/plugin/nvim-web-devicons.vim @@ -0,0 +1,12 @@ +if exists('g:loaded_devicons') | finish | endif + +let s:save_cpo = &cpo +set cpo&vim + +" TODO change so its easier to get +let g:nvim_web_devicons = 1 + +let &cpo = s:save_cpo +unlet s:save_cpo + +let g:loaded_devicons = 1 diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/scripts/align.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/scripts/align.lua new file mode 100644 index 0000000..c2a6527 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/scripts/align.lua @@ -0,0 +1,35 @@ +-- Plugin echasnovski/mini.align must be available on &runtimepath +-- +-- The current working directory must be set to the repo root +-- +-- This file should be run from the shell with `make generate` + +require("mini.align").setup {} + +-- https://github.com/echasnovski/mini.align/blob/main/lua/mini/align.lua#L633C9-L640C8 +local squash_spaces = function(strings) + for i, s in ipairs(strings) do + strings[i] = s:gsub("()(%s+)", function(n, space) + return n == 1 and space or " " + end) + end +end +local steps = { pre_split = { MiniAlign.new_step("squash", squash_spaces) } } + +local function align_table() + local lines = vim.api.nvim_buf_get_lines(0, 1, -2, true) + table.sort(lines) + local aligned_lines = MiniAlign.align_strings(lines, { split_pattern = "%s+" }, steps) + vim.api.nvim_buf_set_lines(0, 1, -2, true, aligned_lines) +end + +for _, theme in ipairs { "default", "light" } do + for _, file in ipairs(_G.ICON_FILES) do + local f = string.format("%s/%s", theme, file) + io.write(string.format("Aligning %s...", f)) + vim.cmd(string.format("noswapfile drop lua/nvim-web-devicons/%s", f)) + align_table() + io.write " OK\n" + vim.cmd "silent! w!" + end +end diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/scripts/filetypes.sh b/.config/nvim/pack/tree/start/nvim-web-devicons/scripts/filetypes.sh new file mode 100755 index 0000000..e016468 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/scripts/filetypes.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# Iterate over the elements of icons_by_file_extension and check if there are missed filetypes. +# $VIMRUNTIME specifies neovim runtime path, defaults to "/usr/share/nvim/runtime" if unset. + +: "${VIMRUNTIME:=/usr/share/nvim/runtime}" + +exit_value=0 + +while read -r key; do + # Search for the key in the filetype to icon table + line=$(grep -F "\"$key\"" "lua/nvim-web-devicons/filetypes.lua") + if [ -z "$line" ]; then + [ -f "${VIMRUNTIME}/syntax/$key.vim" ] && + echo -e "\e[33mPlease add \"$key\" to Lua table in lua/nvim-web-devicons/filetypes.lua.\e[0m" && + exit_value=1 + fi +done < <( + sed -nr 's/\s\s\["(.*)"\].*/\1/p' lua/nvim-web-devicons/default/icons_by_file_extension.lua +) + +exit $exit_value diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/scripts/generate.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/scripts/generate.lua new file mode 100644 index 0000000..4af8606 --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/scripts/generate.lua @@ -0,0 +1,141 @@ +-- Plugin lifepillar/vim-colortemplate must be available on &runtimepath +-- +-- The current working directory must be set to the repo root +-- +-- This file should be run from the shell with `make generate` + +vim.opt.wrapscan = false -- don't wrap after reaching end of file + +local fn = vim.fn + +--- Exit vim +--- @param msg string +--- @param rc number +local function error_exit(msg, rc) + print(msg .. "\n") + vim.cmd("cq " .. rc) +end + +_G.ICON_FILES = { + "icons_by_desktop_environment.lua", + "icons_by_file_extension.lua", + "icons_by_filename.lua", + "icons_by_operating_system.lua", + "icons_by_window_manager.lua", +} + +for _, file in ipairs(_G.ICON_FILES) do + local f = "lua/nvim-web-devicons/default/" .. file + if fn.filereadable(f) == 0 then + error_exit(f, 1) + end +end + +if not jit then + error_exit("Neovim must be LuaJIT-enabled to source this script", 1) +end + +if fn.filereadable "lua/nvim-web-devicons.lua" == 0 then + error_exit("lua/nvim-web-devicons.lua not found", 1) +end + +local rc, err = pcall(vim.fn["colortemplate#colorspace#approx"], "#000000") +if not rc then + error_exit(err .. "\nlifepillar/vim-colortemplate not present in &runtimepath '" .. vim.o.runtimepath .. "'", 1) +end + +-- Needed in order to have the correct indentation on line insertion +vim.o.autoindent = true + +-------------------------------------------------------------------------------- +-- Local functions +-------------------------------------------------------------------------------- + +local light78 = 255 * 7 / 8 +local light68 = 255 * 6 / 8 +local light58 = 255 * 5 / 8 +local light12 = 255 / 2 +local light13 = 255 / 3 + +local function darken_color(rrggbb) + local r, g, b = rrggbb:match "%#(%x%x)(%x%x)(%x%x)" + r, g, b = tonumber("0x" .. r), tonumber("0x" .. g), tonumber("0x" .. b) + -- luminance formula: see https://stackoverflow.com/a/596243 + local lum = 0.299 * r + 0.587 * g + 0.114 * b + if lum < light13 then -------------------- darkest tertile + return rrggbb + elseif lum < light12 then ---------------- second darkest quartile + r = bit.tohex(r / 4 * 3):sub(-2) + g = bit.tohex(g / 4 * 3):sub(-2) + b = bit.tohex(b / 4 * 3):sub(-2) + elseif lum < light58 then ---------------- lightest octiles: first + r = bit.tohex(r / 3 * 2):sub(-2) + g = bit.tohex(g / 3 * 2):sub(-2) + b = bit.tohex(b / 3 * 2):sub(-2) + elseif lum < light68 then ---------------- lightest octiles: second + r = bit.tohex(r / 2):sub(-2) + g = bit.tohex(g / 2):sub(-2) + b = bit.tohex(b / 2):sub(-2) + elseif lum < light78 then ---------------- lightest octiles: third + r = bit.tohex(r / 3):sub(-2) + g = bit.tohex(g / 3):sub(-2) + b = bit.tohex(b / 3):sub(-2) + else ------------------------------------- lightest octile + r = bit.tohex(r / 5):sub(-2) + g = bit.tohex(g / 5):sub(-2) + b = bit.tohex(b / 5):sub(-2) + end + return string.format("#%s%s%s", r, g, b):upper() +end + +local function iterate_colors(proc) + -- move to first line + vim.cmd ":1" + local cursor = fn.search "\\scolor =" + -- fn.search will return 0 when no more matches are found with falsy `wrapscan` + while cursor ~= 0 do + local rrggbb = vim.api.nvim_get_current_line():match '"(#%x%x%x%x%x%x)"' + proc(rrggbb) + vim.cmd "normal! $" + cursor = fn.search "\\scolor =" + end +end + +local function generate_cterm(rrggbb) + local cterm_color = fn["colortemplate#colorspace#approx"](rrggbb).index + vim.cmd(string.format('s/cterm_color = "[0-9]*"/cterm_color = %q', cterm_color)) +end + +local function generate_for_light_bg(rrggbb) + local darkened_rrggbb = darken_color(rrggbb) + vim.cmd(string.format("s/%q/%q/", rrggbb, darkened_rrggbb)) + generate_cterm(darkened_rrggbb) +end + +-------------------------------------------------------------------------------- +-- Update cterm_color for dark background +-------------------------------------------------------------------------------- +for _, file in ipairs(_G.ICON_FILES) do + vim.cmd(string.format("noswapfile drop lua/nvim-web-devicons/default/%s", file)) + io.write(string.format("Generating cterm colors for dark background: %s...", file)) + iterate_colors(generate_cterm) + vim.cmd "silent! wall!" + io.write " OK\n" +end + +-------------------------------------------------------------------------------- +-- Update color and cterm_color for light backgrounds +-------------------------------------------------------------------------------- +for _, file in ipairs(_G.ICON_FILES) do + vim.cmd("noswapfile drop lua/nvim-web-devicons/light/" .. file) + io.write("Generating colors for light background: " .. file .. "...") + iterate_colors(generate_for_light_bg) + vim.cmd( + string.format( + "1s/.*/& -- this file is generated from lua\\/nvim-web-devicons\\/default\\/%s, please do not edit", + file + ) + ) + vim.cmd "silent! wall!" + io.write " OK\n" +end diff --git a/.config/nvim/pack/tree/start/nvim-web-devicons/scripts/sort_filetypes.lua b/.config/nvim/pack/tree/start/nvim-web-devicons/scripts/sort_filetypes.lua new file mode 100644 index 0000000..d6281fd --- /dev/null +++ b/.config/nvim/pack/tree/start/nvim-web-devicons/scripts/sort_filetypes.lua @@ -0,0 +1,11 @@ +-- The current working directory must be set to the repo root +-- +-- This file should be run from the shell with `make generate` + +io.write "Sorting filetypes.lua..." +vim.cmd "noswapfile drop lua/nvim-web-devicons/filetypes.lua" +local lines = vim.api.nvim_buf_get_lines(0, 3, -2, true) +table.sort(lines) +vim.api.nvim_buf_set_lines(0, 3, -2, true, lines) +io.write " OK\n" +vim.cmd "silent! w!" diff --git a/.config/nvim/pack/tree/start/plenary.nvim/.github/FUNDING.yml b/.config/nvim/pack/tree/start/plenary.nvim/.github/FUNDING.yml new file mode 100644 index 0000000..dc9d7fb --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/.github/FUNDING.yml @@ -0,0 +1 @@ +github: tjdevries diff --git a/.config/nvim/pack/tree/start/plenary.nvim/.github/workflows/default.yml b/.config/nvim/pack/tree/start/plenary.nvim/.github/workflows/default.yml new file mode 100644 index 0000000..3ea01aa --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/.github/workflows/default.yml @@ -0,0 +1,68 @@ +name: default + +on: [push, pull_request] + +jobs: + run_tests: + name: unit tests + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + rev: nightly/nvim-linux-x86_64.tar.gz + - os: ubuntu-22.04 + rev: v0.8.3/nvim-linux64.tar.gz + - os: ubuntu-22.04 + rev: v0.9.5/nvim-linux64.tar.gz + - os: ubuntu-22.04 + rev: v0.10.4/nvim-linux-x86_64.tar.gz + steps: + - uses: actions/checkout@v4 + - run: date +%F > todays-date + - name: Restore cache for today's nightly. + uses: actions/cache@v4 + with: + path: _neovim + key: ${{ runner.os }}-${{ matrix.rev }}-${{ hashFiles('todays-date') }} + - name: Prepare + run: | + test -d _neovim || { + mkdir -p _neovim + curl -sL "https://github.com/neovim/neovim/releases/download/${{ matrix.rev }}" | tar xzf - --strip-components=1 -C "${PWD}/_neovim" + } + + - name: Run tests + run: | + export PATH="${PWD}/_neovim/bin:${PATH}" + export VIM="${PWD}/_neovim/share/nvim/runtime" + nvim --version + make test + + stylua: + name: stylua + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: JohnnyMorganz/stylua-action@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + version: latest + # CLI arguments + args: --color always --check . + + luacheck: + name: Luacheck + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Prepare + run: | + sudo apt-get update + sudo apt-get install -y luarocks + sudo luarocks install luacheck + + - name: Lint + run: sudo make lint diff --git a/.config/nvim/pack/tree/start/plenary.nvim/.github/workflows/release.yml b/.config/nvim/pack/tree/start/plenary.nvim/.github/workflows/release.yml new file mode 100644 index 0000000..cf8a766 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/.github/workflows/release.yml @@ -0,0 +1,21 @@ +name: "release" +on: + push: + tags: + - 'v*' +jobs: + luarocks-upload: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: LuaRocks Upload + uses: nvim-neorocks/luarocks-tag-release@v1.0.2 + env: + LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }} + with: + summary: "lua functions you don't want to write" + detailed_description: | + plenary: full; complete; entire; absolute; unqualified. + All the lua functions I don't want to write twice. + template: "./rockspec.template" + diff --git a/.config/nvim/pack/tree/start/plenary.nvim/.gitignore b/.config/nvim/pack/tree/start/plenary.nvim/.gitignore new file mode 100644 index 0000000..26def9e --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/.gitignore @@ -0,0 +1,43 @@ +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +build/ +doc/tags diff --git a/.config/nvim/pack/tree/start/plenary.nvim/.luacheckrc b/.config/nvim/pack/tree/start/plenary.nvim/.luacheckrc new file mode 100644 index 0000000..0bd6ca4 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/.luacheckrc @@ -0,0 +1,60 @@ +-- Rerun tests only if their modification time changed. +cache = true + +std = luajit +codes = true + +self = false + +-- Glorious list of warnings: https://luacheck.readthedocs.io/en/stable/warnings.html +ignore = { + "212", -- Unused argument, In the case of callback function, _arg_name is easier to understand than _, so this option is set to off. + "122", -- Indirectly setting a readonly global +} + +globals = { + "_", + "_PlenaryLeafTable", + "_PlenaryBustedOldAssert", + "_AssociatedBufs", +} + +-- Global objects defined by the C code +read_globals = { + "vim", +} + +exclude_files = { + "lua/plenary/profile/lua_profiler.lua", + "lua/plenary/profile/memory_profiler.lua", + "lua/plenary/async_lib/*.lua", +} + +files = { + ["lua/plenary/busted.lua"] = { + globals = { + "describe", + "it", + "pending", + "before_each", + "after_each", + "clear", + "assert", + "print", + }, + }, + ["lua/plenary/async/init.lua"] = { + globals = { + "a", + }, + }, + ["lua/plenary/async/tests.lua"] = { + globals = { + "describe", + "it", + "pending", + "before_each", + "after_each", + }, + }, +} diff --git a/.config/nvim/pack/tree/start/plenary.nvim/.stylua.toml b/.config/nvim/pack/tree/start/plenary.nvim/.stylua.toml new file mode 100644 index 0000000..ecb6dca --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/.stylua.toml @@ -0,0 +1,6 @@ +column_width = 120 +line_endings = "Unix" +indent_type = "Spaces" +indent_width = 2 +quote_style = "AutoPreferDouble" +call_parentheses = "None" diff --git a/.config/nvim/pack/tree/start/plenary.nvim/.styluaignore b/.config/nvim/pack/tree/start/plenary.nvim/.styluaignore new file mode 100644 index 0000000..a817c12 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/.styluaignore @@ -0,0 +1,10 @@ +build/ +data/ +lua/luassert +lua/plenary/profile.lua +lua/plenary/profile/ +lua/plenary/bit.lua +lua/plenary/_meta/_luassert.lua +lua/say.lua +scratch/ +scripts/ diff --git a/.config/nvim/pack/tree/start/plenary.nvim/LICENSE b/.config/nvim/pack/tree/start/plenary.nvim/LICENSE new file mode 100644 index 0000000..f8e3950 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 TJ DeVries + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.config/nvim/pack/tree/start/plenary.nvim/Makefile b/.config/nvim/pack/tree/start/plenary.nvim/Makefile new file mode 100644 index 0000000..304e1f8 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/Makefile @@ -0,0 +1,12 @@ +.PHONY: test generate_filetypes lint luarocks_upload test_luarocks_install +test: + nvim --headless --noplugin -u scripts/minimal.vim -c "PlenaryBustedDirectory tests/plenary/ {minimal_init = 'tests/minimal_init.vim', sequential = true}" + +generate_filetypes: + nvim --headless -c 'luafile scripts/update_filetypes_from_github.lua' -c 'qa!' + +generate_luassert_types: + nvim --headless -c 'luafile scripts/generate_luassert_types.lua' -c 'qa!' + +lint: + luacheck lua/plenary diff --git a/.config/nvim/pack/tree/start/plenary.nvim/POPUP.md b/.config/nvim/pack/tree/start/plenary.nvim/POPUP.md new file mode 100644 index 0000000..c56df43 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/POPUP.md @@ -0,0 +1,89 @@ +# Popup tracking + +[WIP] An implementation of the Popup API from vim in Neovim. Hope to upstream +when complete + +## Goals + +Provide an API that is compatible with the vim `popup_*` APIs. After +stablization and any required features are merged into Neovim, we can upstream +this and expose the API in vimL to create better compatibility. + +## Notices +- **2021-09-19:** we now follow Vim's convention of the first line/column of the screen being indexed 1, so that 0 can be used for centering. +- **2021-08-19:** we now follow Vim's default to `noautocmd` on popup creation. This can be overriden with `vim_options.noautocmd=false` + +## List of Neovim Features Required: + +- [ ] Key handlers (used for `popup_filter`) +- [ ] scrollbar for floating windows + - [ ] scrollbar + - [ ] scrollbarhighlight + - [ ] thumbhighlight + +Optional: + +- [ ] Add forced transparency to a floating window. + - Apparently overrides text? + - This is for the `mask` feature flag + + +Unlikely (due to technical difficulties): + +- [ ] Add `textprop` wrappers? + - textprop + - textpropwin + - textpropid +- [ ] "close" + - But this is mostly because I don't know how to use mouse APIs in nvim. If someone knows. please make an issue in the repo, and maybe we can get it sorted out. + +Unlikely (due to not sure if people are using): +- [ ] tabpage + +## Progress + +Suported Features: + +- [x] what + - string + - list of strings +- [x] popup_create-arguments + - [x] border + - [x] borderchars + - [x] col + - [x] cursorline + - [x] highlight + - [x] line + - [x] {max,min}{height,width} + - [?] moved + - [x] "any" + - [ ] "word" + - [ ] "WORD" + - [ ] "expr" + - [ ] (list options) + - [x] padding + - [?] pos + - Somewhat implemented. Doesn't work with borders though. + - [x] posinvert + - [x] time + - [x] title + - [x] wrap + - [x] zindex + +## All known unimplemented vim features at the moment + +- firstline +- hidden +- ~ pos +- fixed +- filter +- filtermode +- mapping +- callback +- mouse: + - mousemoved + - close + - drag + - resize + +- (not implemented in vim yet) flip diff --git a/.config/nvim/pack/tree/start/plenary.nvim/README.md b/.config/nvim/pack/tree/start/plenary.nvim/README.md new file mode 100644 index 0000000..85cbb8d --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/README.md @@ -0,0 +1,398 @@ +# plenary.nvim + +All the lua functions I don't want to write twice. + +> plenary: +> +> full; complete; entire; absolute; unqualified. + +Note that this library is useless outside of Neovim since it requires Neovim functions. It should be usable with any recent version of Neovim though. + +At the moment, it is very much in pre-alpha :smile: Expect changes to the way some functions are structured. I'm hoping to finish some document generators to provide better documentation for people to use and consume and then at some point we'll stabilize on a few more stable APIs. + +## Installation + +[![LuaRocks](https://img.shields.io/luarocks/v/Conni2461/plenary.nvim?logo=lua&color=purple)](https://luarocks.org/modules/Conni2461/plenary.nvim) + +Using [plug](https://github.com/junegunn/vim-plug): + +```vim +Plug 'nvim-lua/plenary.nvim' +``` + +Using [packer](https://github.com/wbthomason/packer.nvim): + +``` +use "nvim-lua/plenary.nvim" +``` + +## Modules + +- [plenary.async](#plenaryasync) +- [plenary.async_lib](#plenaryasync_lib) +- [plenary.job](#plenaryjob) +- [plenary.path](#plenarypath) +- [plenary.scandir](#plenaryscandir) +- [plenary.context_manager](#plenarycontext_manager) +- [plenary.test_harness](#plenarytest_harness) +- [plenary.filetype](#plenaryfiletype) +- [plenary.strings](#plenarystrings) + +### plenary.async + +A Lua module for asynchronous programming using coroutines. This library is built on native lua coroutines and `libuv`. Coroutines make it easy to avoid callback hell and allow for easy cooperative concurrency and cancellation. Apart from allowing users to perform asynchronous io easily, this library also functions as an abstraction for coroutines. + +#### Getting started + +You can do +```lua +local async = require "plenary.async" +``` +All other modules are automatically required and can be accessed by indexing `async`. +You needn't worry about performance as this will require all the submodules lazily. + +#### A quick example + +Libuv luv provides this example of reading a file. + +```lua +local uv = vim.loop + +local read_file = function(path, callback) + uv.fs_open(path, "r", 438, function(err, fd) + assert(not err, err) + uv.fs_fstat(fd, function(err, stat) + assert(not err, err) + uv.fs_read(fd, stat.size, 0, function(err, data) + assert(not err, err) + uv.fs_close(fd, function(err) + assert(not err, err) + callback(data) + end) + end) + end) + end) +end +``` + +We can write it using the library like this: +```lua +local a = require "plenary.async" + +local read_file = function(path) + local err, fd = a.uv.fs_open(path, "r", 438) + assert(not err, err) + + local err, stat = a.uv.fs_fstat(fd) + assert(not err, err) + + local err, data = a.uv.fs_read(fd, stat.size, 0) + assert(not err, err) + + local err = a.uv.fs_close(fd) + assert(not err, err) + + return data +end +``` + +#### Plugins using this + +- [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) +- [vgit.nvim](https://github.com/tanvirtin/vgit.nvim) +- [neogit](https://github.com/TimUntersberger/neogit) +- [neo-tree.nvim](https://github.com/nvim-neo-tree/neo-tree.nvim) + +### plenary.async_lib + +Please use `plenary.async` instead. This was version 1 and is just here for compatibility reasons. + +### plenary.async.control.channel.oneshot + +Creates a oneshot channel. It can only send data one time. + +The sender is not async while the receiver is. + +Example: + +```lua +local a = require'plenary.async' +local tx, rx = a.control.channel.oneshot() + +a.run(function() + local ret = long_running_fn() + tx(ret) +end) + +local ret = rx() +``` + +### plenary.async.control.channel.mpsc + +Creates a multiple producer single consumer channel. + +Example: + +```lua +local a = require'plenary.async' +local sender, receiver = a.control.channel.mpsc() + +a.run(function() + sender.send(10) + sender.send(20) +end) + +a.run(function() + sender.send(30) + sender.send(40) +end) + +for _ = 1, 4 do + local value = receiver.recv() + print('received:', value) +end +``` + +### plenary.job + +A Lua module to interact with system processes. Pass in your `command`, the desired `args`, `env` and `cwd`. +Define optional callbacks for `on_stdout`, `on_stderr` and `on_exit` and `start` your Job. + +Note: Each job has an empty environment. + +```lua +local Job = require'plenary.job' + +Job:new({ + command = 'rg', + args = { '--files' }, + cwd = '/usr/bin', + env = { ['a'] = 'b' }, + on_exit = function(j, return_val) + print(return_val) + print(j:result()) + end, +}):sync() -- or start() +``` + +### plenary.path + +A Lua module that implements a bunch of the things from `pathlib` from Python, so that paths are easy to work with. + +### plenary.scandir + +`plenary.scandir` is fast recursive file operations. It is similar to unix `find` or `fd` in that it can do recursive scans over a given directory, or a set of directories. + +It offers a wide range of opts for limiting the depth, show hidden and more. `plenary.scan_dir` can be ran synchronously and asynchronously and offers `on_insert(file, typ)` and `on_exit(files)` callbacks. `on_insert(file, typ)` is available for both while `on_exit(files)` is only available for async. + +```lua +local scan = require'plenary.scandir' +scan.scan_dir('.', { hidden = true, depth = 2 }) +``` + +This module also offers `ls -la` sync and async functions that will return a formated string for all files in the directory. +Why? Just for fun + +### plenary.context_manager + +Implements `with` and `open` just like in Python. For example: + +```lua +local with = context_manager.with +local open = context_manager.open + +local result = with(open("README.md"), function(reader) + return reader:read() +end) + +assert(result == "# plenary.nvim") +``` + + +### plenary.test_harness + +`:help plenary-test` + +Supports (simple) busted-style testing. It implements a mock-ed busted interface, that will allow you to run simple +busted style tests in separate neovim instances. + +To run the current spec file in a floating window, you can use the keymap `PlenaryTestFile`. For example: + +``` +nmap t PlenaryTestFile +``` +In this case, the test is run with a minimal configuration, that includes in +its runtimepath only plenary.nvim and the current working directory. + +To run a whole directory from the command line, you could do something like: + +``` +nvim --headless -c "PlenaryBustedDirectory tests/plenary/ {options}" +``` + +Where the first argument is the directory you'd like to test. It will search for files with +the pattern `*_spec.lua` and execute them in separate neovim instances. + +Without second argument, `PlenaryBustedDirectory` is also run with a minimal +configuration. Otherwise it is a Lua option table with the following fields: +- `nvim_cmd`: specify the command to launch this neovim instance (defaults to `vim.v.progpath`) +- `init`: specify an init.vim to use for this instance +- `minimal_init`: as for `init`, but also run the neovim instance with `--noplugin` +- `sequential`: whether to run tests sequentially (default is to run in parallel) +- `keep_going`: if `sequential`, whether to continue on test failure (default true) +- `timeout`: controls the maximum time allotted to each job in parallel or + sequential operation (defaults to 50,000 milliseconds) + +The exit code is 0 when success and 1 when fail, so you can use it easily in a `Makefile`! + + +NOTE: + +So far, the only supported busted items are: + +- `describe` +- `it` +- `pending` +- `before_each` +- `after_each` +- `clear` +- `assert.*` etc. (from luassert, which is bundled) + +OTHER NOTE: + +We used to support `luaunit` and original `busted` but it turns out it was way too hard and not worthwhile +for the difficulty of getting them setup, particularly on other platforms or in CI. Now, we have a dep free +(or at least, no other installation steps necessary) `busted` implementation that can be used more easily. + +Please take a look at the new APIs and make any issues for things that aren't clear. I am happy to fix them +and make it work well :) + +OTHER OTHER NOTE: +Take a look at some test examples [here](TESTS_README.md). + +#### Colors + +You no longer need nvim-terminal to get this to work. We use `nvim_open_term` now. + +### plenary.filetype + +Will detect the filetype based on `extension`/`special filename`/`shebang` or `modeline` + +- `require'plenary.filetype'.detect(filepath, opts)` is a function that does all of above and exits as soon as a filetype is found +- `require'plenary.filetype'.detect_from_extension(filepath)` +- `require'plenary.filetype'.detect_from_name(filepath)` +- `require'plenary.filetype'.detect_from_modeline(filepath)` +- `require'plenary.filetype'.detect_from_shebang(filepath)` + +Add filetypes by creating a new file named `~/.config/nvim/data/plenary/filetypes/foo.lua` and register that file with +`:lua require'plenary.filetype'.add_file('foo')`. Content of the file should look like that: +```lua +return { + extension = { + -- extension = filetype + -- example: + ['jl'] = 'julia', + }, + file_name = { + -- special filenames, likes .bashrc + -- we provide a decent amount + -- name = filetype + -- example: + ['.bashrc'] = 'bash', + }, + shebang = { + -- Shebangs are supported as well. Currently we provide + -- sh, bash, zsh, python, perl with different prefixes like + -- /usr/bin, /bin/, /usr/bin/env, /bin/env + -- shebang = filetype + -- example: + ['/usr/bin/node'] = 'javascript', + } +} +``` + +### plenary.strings + +Re-implement VimL funcs to use them in Lua loop. + +* `strings.strdisplaywidth` +* `strings.strcharpart` + +And some other funcs are here to deal with common problems. + +* `strings.truncate` +* `strings.align_str` +* `strings.dedent` + +### plenary.profile + +Thin wrapper around LuaJIT's [`jit.p` profiler](https://blast.hk/moonloader/luajit/ext_profiler.html). + +```lua +require'plenary.profile'.start("profile.log") + +-- code to be profiled + +require'plenary.profile'.stop() +``` + +You can use `start("profile.log", {flame = true})` to output the log in a +flamegraph-compatible format. A flamegraph can be created from this using +https://github.com/jonhoo/inferno via +``` +inferno-flamegraph profile.log > flame.svg +``` +The resulting interactive SVG file can be viewed in any browser. + +Status: WIP + +### plenary.popup + +See [popup documentation](./POPUP.md) for both progress tracking and implemented APIs. + +### plenary.window + +Window helper functions to wrap some of the more difficult cases. Particularly for floating windows. + +Status: WIP + +### plenary.collections + +Contains pure lua implementations for various standard collections. + +```lua +local List = require 'plenary.collections.py_list' + +local myList = List { 9, 14, 32, 5 } + +for i, v in myList:iter() do + print(i, v) +end + +``` + +Status: WIP + +### Troubleshooting + +If you're having trouble / things are hanging / other problems: + +``` +$ export DEBUG_PLENARY=true +``` + +This will enable debugging for the plugin. + +### plenary.neorocks + +DELETED: Please use packer.nvim or other lua-rocks wrapper instead. This no longer exists. + +### FAQ + +1. Error: Too many open files + +- \*nix systems have a setting to configure the maximum amount of open file + handles. It can occur that the default value is pretty low and that you end up + getting this error after opening a couple of files. On Linux you can see the + current limit with `ulimit -n` and set it with `ulimit -n 4096`. If you're on + macOS the command is `sudo launchctl limit maxfiles 4096 4096`. diff --git a/.config/nvim/pack/tree/start/plenary.nvim/TESTS_README.md b/.config/nvim/pack/tree/start/plenary.nvim/TESTS_README.md new file mode 100644 index 0000000..955cdf9 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/TESTS_README.md @@ -0,0 +1,153 @@ +# Testing Guide + +Some testing examples using Plenary.nvim + +# A simple test + +This tests demonstrates a **describe** block that contains two tests defined with **it** blocks, the describe block also contains a **before_each** call that gets called before each test. + +```lua +describe("some basics", function() + + local bello = function(boo) + return "bello " .. boo + end + + local bounter + + before_each(function() + bounter = 0 + end) + + it("some test", function() + bounter = 100 + assert.equals("bello Brian", bello("Brian")) + end) + + it("some other test", function() + assert.equals(0, bounter) + end) +end) +``` + +The test **some test** checks that a functions output is as expected based on the input. The second test **some other test** checks that the variable **bounter** is reset for each test (as defined in the before_each block). + +# Running tests + +Run the test using `:PlenaryBustedFile `. + +```vimscript +" Run the test in the current buffer +:PlenaryBustedFile % +" Run all tests in the directory "tests/plenary/" +:PlenaryBustedDirectory tests/plenary/ +``` + +Or you can run tests in headless mode to see output in terminal: + +```bash +# run all tests in terminal +cd plenary.nvim +nvim --headless -c 'PlenaryBustedDirectory tests' +``` + +# mocking with luassert + +Plenary.nvim comes bundled with [luassert](https://github.com/Olivine-Labs/luassert) a library that's built to extend the built-int assertions... but it also comes with stubs, mocks and spies! + +Sometimes it's useful to test functions that have nvim api function calls within them, take for example the following example of a simple module that creates a new buffer and opens in it in a split. + + +**module.lua** +```lua +local M = {} + +function M.realistic_func() + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_command("sbuffer " .. buf) +end + +return M +``` + +The following is an example of completely mocking a module, and another of just stubbing a single function within a module. In this case the module is `vim.api`, with an aim of giving an example of a unit test (fully mocked) and an integration test... details in the comments. + +**module.lua** +```lua +-- import the luassert.mock module +local mock = require('luassert.mock') +local stub = require('luassert.stub') + +describe("example", function() + -- instance of module to be tested + local testModule = require('example.module') + -- mocked instance of api to interact with + + describe("realistic_func", function() + it("Should make expected calls to api, fully mocked", function() + -- mock the vim.api + local api = mock(vim.api, true) + + -- set expectation when mocked api call made + api.nvim_create_buf.returns(5) + + testModule.realistic_func() + + -- assert api was called with expcted values + assert.stub(api.nvim_create_buf).was_called_with(false, true) + -- assert api was called with set expectation + assert.stub(api.nvim_command).was_called_with("sbuffer 5") + + -- revert api back to it's former glory + mock.revert(api) + end) + + it("Should mock single api call", function() + -- capture some number of windows and buffers before + -- running our function + local buf_count = #vim.api.nvim_list_bufs() + local win_count = #vim.api.nvim_list_wins() + -- stub a single function in the api + stub(vim.api, "nvim_command") + + testModule.realistic_func() + + -- capture some details after running out function + local after_buf_count = #vim.api.nvim_list_bufs() + local after_win_count = #vim.api.nvim_list_wins() + + -- why 3 not two? NO IDEA! The point is we mocked + -- nvim_commad and there is only a single window + assert.equals(3, buf_count) + assert.equals(4, after_buf_count) + + -- WOOPIE! + assert.equals(1, win_count) + assert.equals(1, after_win_count) + end) + end) +end) +``` + +To test this in your `~/.config/nvim` configuration, try the suggested file structure: + +``` +lua/example/module.lua +lua/spec/example/module_spec.lua +``` + +# Asynchronous testing + +Tests run in a coroutine, which can be yielded and resumed. This can be used to +test code that uses asynchronous Neovim functionalities. For example, this can +be done inside a test: + +```lua +local co = coroutine.running() +vim.defer_fn(function() + coroutine.resume(co) +end, 1000) +--The test will reach here immediately. +coroutine.yield() +--The test will only reach here after one second, when the deferred function runs. +``` diff --git a/.config/nvim/pack/tree/start/plenary.nvim/data/plenary/filetypes/base.lua b/.config/nvim/pack/tree/start/plenary.nvim/data/plenary/filetypes/base.lua new file mode 100644 index 0000000..1adffb3 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/data/plenary/filetypes/base.lua @@ -0,0 +1,886 @@ +return { + extension = { + ['ncl'] = [[text]], + ['ph'] = [[perl]], + ['al'] = [[perl]], + ['cl'] = [[lisp]], + ['vhi'] = [[vhdl]], + ['sublime-snippet'] = [[xml]], + ['tcl'] = [[tcl]], + ['pp'] = [[pascal]], + ['builds'] = [[xml]], + ['lua'] = [[lua]], + ['pkb'] = [[plsql]], + ['wl'] = [[mma]], + ['6'] = [[groff]], + ['scm'] = [[scheme]], + ['ml'] = [[ocaml]], + ['filters'] = [[xml]], + ['st'] = [[html]], + ['ksy'] = [[yaml]], + ['mt'] = [[mma]], + ['ada'] = [[ada]], + ['vho'] = [[vhdl]], + ['nawk'] = [[awk]], + ['3pm'] = [[groff]], + ['maxhelp'] = [[json]], + ['ct'] = [[xml]], + ['ipp'] = [[cpp]], + ['a51'] = [[asm]], + ['meta'] = [[yaml]], + ['fsproj'] = [[xml]], + ['ccxml'] = [[xml]], + ['ado'] = [[stata]], + ['mli'] = [[ocaml]], + ['r2'] = [[rebol]], + ['csl'] = [[xml]], + ['bbx'] = [[tex]], + ['xsjslib'] = [[javascript]], + ['e'] = [[eiffel]], + ['tac'] = [[python]], + ['mustache'] = [[smarty]], + ['zcml'] = [[xml]], + ['glade'] = [[xml]], + ['cuh'] = [[cuda]], + ['cxx'] = [[cpp]], + ['urdf'] = [[xml]], + ['ditaval'] = [[xml]], + ['cscfg'] = [[xml]], + ['j2'] = [[django]], + ['pkgproj'] = [[xml]], + ['ma'] = [[mma]], + ['cljs.hl'] = [[clojure]], + ['brd'] = [[xml]], + ['asn'] = [[asn]], + ['xspec'] = [[xml]], + ['db2'] = [[sql]], + ['sjs'] = [[javascript]], + ['m'] = [[matlab]], + ['gdbinit'] = [[gdb]], + ['re'] = [[cpp]], + ['adoc'] = [[asciidoc]], + ['pyde'] = [[python]], + ['mjml'] = [[xml]], + ['workbook'] = [[markdown]], + ['ssjs'] = [[javascript]], + ['dircolors'] = [[dircolors]], + ['ui'] = [[xml]], + ['ant'] = [[xml]], + ['wast'] = [[wast]], + ['_js'] = [[javascript]], + ['7'] = [[groff]], + ['dylan'] = [[dylan]], + ['jsproj'] = [[xml]], + ['jsb'] = [[javascript]], + ['xml'] = [[xml]], + ['tcsh'] = [[tcsh]], + ['xpy'] = [[python]], + ['wisp'] = [[clojure]], + ['tm'] = [[tcl]], + ['plot'] = [[gnuplot]], + ['mjs'] = [[javascript]], + ['cjs'] = [[javascript]], + ['env'] = [[sh]], + ['jsfl'] = [[javascript]], + ['eye'] = [[ruby]], + ['jinja2'] = [[django]], + ['lookml'] = [[yaml]], + ['mcmeta'] = [[json]], + ['workflow'] = [[xml]], + ['eq'] = [[cs]], + ['storyboard'] = [[xml]], + ['nbp'] = [[mma]], + ['maxpat'] = [[json]], + ['yy'] = [[yacc]], + ['h++'] = [[cpp]], + ['asc'] = [[asciidoc]], + ['phps'] = [[php]], + ['cabal'] = [[cabal]], + ['xacro'] = [[xml]], + ['dhall'] = [[haskell]], + ['vba'] = [[vim]], + ['feature'] = [[cucumber]], + ['psc1'] = [[xml]], + ['plb'] = [[plsql]], + ['logtalk'] = [[logtalk]], + ['pascal'] = [[pascal]], + ['smk'] = [[python]], + ['rst.txt'] = [[rst]], + ['svh'] = [[systemverilog]], + ['sthlp'] = [[stata]], + ['nb'] = [[text]], + ['hrl'] = [[erlang]], + ['f'] = [[forth]], + ['rb'] = [[ruby]], + ['xsd'] = [[xml]], + ['cfg'] = [[dosini]], + ['1in'] = [[groff]], + ['mkiv'] = [[tex]], + ['mask'] = [[yaml]], + ['cproject'] = [[xml]], + ['gnuplot'] = [[gnuplot]], + ['sagews'] = [[python]], + ['nsh'] = [[nsis]], + ['njk'] = [[django]], + ['pic'] = [[pic]], + ['man'] = [[groff]], + ['owl'] = [[xml]], + ['ino'] = [[cpp]], + ['dtx'] = [[tex]], + ['volt'] = [[d]], + ['ltx'] = [[tex]], + ['rtf'] = [[rtf]], + ['jscad'] = [[javascript]], + ['8'] = [[groff]], + ['3qt'] = [[groff]], + ['n'] = [[groff]], + ['lisp'] = [[lisp]], + ['forth'] = [[forth]], + ['dll.config'] = [[xml]], + ['prefab'] = [[yaml]], + ['ads'] = [[ada]], + ['jake'] = [[javascript]], + ['zsh'] = [[sh]], + ['v'] = [[verilog]], + ['ini'] = [[dosini]], + ['command'] = [[sh]], + ['fr'] = [[forth]], + ['yacc'] = [[yacc]], + ['ccproj'] = [[xml]], + ['xqy'] = [[xquery]], + ['rabl'] = [[ruby]], + ['nr'] = [[groff]], + ['wsgi'] = [[python]], + ['model.lkml'] = [[yaml]], + ['lsl'] = [[lsl]], + ['yyp'] = [[json]], + ['yaml-tmlanguage'] = [[yaml]], + ['rbi'] = [[ruby]], + ['srt'] = [[lisp]], + ['unity'] = [[yaml]], + ['vhs'] = [[vhdl]], + ['yap'] = [[prolog]], + ['php5'] = [[php]], + ['pas'] = [[pascal]], + ['gyp'] = [[python]], + ['jsonl'] = [[json]], + ['cc'] = [[cpp]], + ['grt'] = [[groovy]], + ['txi'] = [[texinfo]], + ['asm'] = [[asm]], + ['plx'] = [[perl]], + ['prefs'] = [[dosini]], + ['mly'] = [[ocaml]], + ['xsl'] = [[xslt]], + ['osm'] = [[xml]], + ['sc'] = [[scala]], + ['au3'] = [[autoit]], + ['obj'] = [[obj]], + ['uc'] = [[java]], + ['lhs'] = [[lhaskell]], + ['1'] = [[groff]], + ['resx'] = [[xml]], + ['geojson'] = [[json]], + ['tmux'] = [[sh]], + ['rg'] = [[clojure]], + ['tcc'] = [[cpp]], + ['cbl'] = [[cobol]], + ['wat'] = [[wast]], + ['com'] = [[dcl]], + ['podspec'] = [[ruby]], + ['no'] = [[text]], + ['spec'] = [[python]], + ['vhw'] = [[vhdl]], + ['maxproj'] = [[json]], + ['xbm'] = [[c]], + ['wxl'] = [[xml]], + ['mk'] = [[make]], + ['epj'] = [[json]], + ['do'] = [[stata]], + ['rexx'] = [[rexx]], + ['ms'] = [[groff]], + ['w'] = [[cweb]], + ['ss'] = [[scheme]], + ['desktop.in'] = [[desktop]], + ['po'] = [[po]], + ['mdpolicy'] = [[xml]], + ['mathematica'] = [[mma]], + ['vw'] = [[plsql]], + ['rss'] = [[xml]], + ['cs'] = [[cs]], + ['mkdown'] = [[markdown]], + ['gs'] = [[javascript]], + ['doh'] = [[stata]], + ['vbproj'] = [[xml]], + ['sfproj'] = [[xml]], + ['mat'] = [[yaml]], + ['cmake.in'] = [[cmake]], + ['rest.txt'] = [[rst]], + ['sls'] = [[scheme]], + ['fxml'] = [[xml]], + ['jss'] = [[javascript]], + ['avsc'] = [[json]], + ['mbox'] = [[basic]], + ['cake'] = [[cs]], + ['gst'] = [[xml]], + ['p6l'] = [[perl6]], + ['cdf'] = [[mma]], + ['pyi'] = [[python]], + ['ahk'] = [[autohotkey]], + ['tmac'] = [[groff]], + ['xaml'] = [[xml]], + ['texi'] = [[texinfo]], + ['udf'] = [[sql]], + ['rebol'] = [[rebol]], + ['gvy'] = [[groovy]], + ['h'] = [[c]], + ['pm6'] = [[perl6]], + ['ivy'] = [[xml]], + ['properties'] = [[dosini]], + ['eliomi'] = [[ocaml]], + ['nanorc'] = [[nanorc]], + ['sps'] = [[scheme]], + ['nproj'] = [[xml]], + ['ch'] = [[clipper]], + ['odd'] = [[xml]], + ['fun'] = [[sml]], + ['mir'] = [[yaml]], + ['nuspec'] = [[xml]], + ['pck'] = [[plsql]], + ['2'] = [[groff]], + ['nl'] = [[lisp]], + ['fth'] = [[forth]], + ['cps'] = [[pascal]], + ['sh'] = [[sh]], + ['rbw'] = [[ruby]], + ['dlm'] = [[idl]], + ['pod'] = [[pod]], + ['clixml'] = [[xml]], + ['duby'] = [[ruby]], + ['gdb'] = [[gdb]], + ['boot'] = [[clojure]], + ['adb'] = [[ada]], + ['tex'] = [[tex]], + ['csx'] = [[cs]], + ['sbt'] = [[scala]], + ['csdef'] = [[xml]], + ['dpr'] = [[pascal]], + ['rex'] = [[rexx]], + ['srdf'] = [[xml]], + ['pl'] = [[perl]], + ['markdown'] = [[markdown]], + ['cgi'] = [[perl]], + ['cp'] = [[cpp]], + ['jinja'] = [[django]], + ['gp'] = [[gnuplot]], + ['x'] = [[rpcgen]], + ['pt'] = [[xml]], + ['tpp'] = [[cpp]], + ['intr'] = [[dylan]], + ['JSON-tmLanguage'] = [[json]], + ['ux'] = [[xml]], + ['fpp'] = [[fortran]], + ['phpt'] = [[php]], + ['4th'] = [[forth]], + ['dita'] = [[xml]], + ['viw'] = [[sql]], + ['vsixmanifest'] = [[xml]], + ['clj'] = [[clojure]], + ['yml.mysql'] = [[yaml]], + ['hpp'] = [[cpp]], + ['xsp.metadata'] = [[xml]], + ['sexp'] = [[lisp]], + ['csproj'] = [[xml]], + ['tab'] = [[sql]], + ['cql'] = [[sql]], + ['abap'] = [[abap]], + ['wlua'] = [[lua]], + ['lex'] = [[lex]], + ['java'] = [[java]], + ['edn'] = [[clojure]], + ['ditamap'] = [[xml]], + ['3p'] = [[groff]], + ['xul'] = [[xml]], + ['cmake'] = [[cmake]], + ['kit'] = [[basic]], + ['rs.in'] = [[rust]], + ['php4'] = [[php]], + ['hxx'] = [[cpp]], + ['cljscm'] = [[clojure]], + ['vcxproj'] = [[xml]], + ['php3'] = [[php]], + ['xml.dist'] = [[xml]], + ['ged'] = [[gedcom]], + ['i'] = [[asm]], + ['xproc'] = [[xml]], + ['ndproj'] = [[xml]], + ['less'] = [[less]], + ['lgt'] = [[logtalk]], + ['sql'] = [[plsql]], + ['cls'] = [[tex]], + ['targets'] = [[xml]], + ['me'] = [[groff]], + ['3x'] = [[groff]], + ['gtpl'] = [[groovy]], + ['prg'] = [[clipper]], + ['natvis'] = [[xml]], + ['3'] = [[groff]], + ['di'] = [[d]], + ['1x'] = [[groff]], + ['mdoc'] = [[groff]], + ['xsjs'] = [[javascript]], + ['iml'] = [[xml]], + ['ctp'] = [[php]], + ['kml'] = [[xml]], + ['eml'] = [[basic]], + ['raml'] = [[raml]], + ['gml'] = [[xml]], + ['yml'] = [[yaml]], + ['xq'] = [[xquery]], + ['sml'] = [[sml]], + ['sublime-syntax'] = [[yaml]], + ['mm'] = [[xml]], + ['y'] = [[yacc]], + ['mu'] = [[mupad]], + ['sld'] = [[scheme]], + ['asd'] = [[lisp]], + ['pgsql'] = [[sql]], + ['nqp'] = [[perl6]], + ['anim'] = [[yaml]], + ['vhf'] = [[vhdl]], + ['cu'] = [[cuda]], + ['make'] = [[make]], + ['pyx'] = [[pyrex]], + ['c++'] = [[cpp]], + ['lslp'] = [[lsl]], + ['rake'] = [[ruby]], + ['webmanifest'] = [[json]], + ['ijs'] = [[j]], + ['hsc'] = [[haskell]], + ['pl6'] = [[perl6]], + ['p6m'] = [[perl6]], + ['ny'] = [[lisp]], + ['mak'] = [[make]], + ['p6'] = [[perl6]], + ['6pm'] = [[perl6]], + ['lfe'] = [[lisp]], + ['6pl'] = [[perl6]], + ['toc'] = [[tex]], + ['yrl'] = [[erlang]], + ['vssettings'] = [[xml]], + ['sty'] = [[tex]], + ['ipynb'] = [[json]], + ['mkvi'] = [[tex]], + ['p'] = [[gnuplot]], + ['lmi'] = [[python]], + ['rockspec'] = [[lua]], + ['vhd'] = [[vhdl]], + ['sieve'] = [[sieve]], + ['lbx'] = [[tex]], + ['1m'] = [[groff]], + ['jelly'] = [[xml]], + ['3m'] = [[groff]], + ['ins'] = [[tex]], + ['xib'] = [[xml]], + ['cbx'] = [[tex]], + ['pd_lua'] = [[lua]], + ['dae'] = [[xml]], + ['emacs.desktop'] = [[lisp]], + ['bison'] = [[yacc]], + ['matlab'] = [[matlab]], + ['emacs'] = [[lisp]], + ['pot'] = [[po]], + ['el'] = [[lisp]], + ['podsl'] = [[lisp]], + ['pac'] = [[javascript]], + ['gitignore'] = [[gitignore]], + ['vue'] = [[vue]], + ['html.hl'] = [[html]], + ['trg'] = [[plsql]], + ['tps'] = [[plsql]], + ['hs-boot'] = [[haskell]], + ['csh'] = [[tcsh]], + ['mawk'] = [[awk]], + ['fan'] = [[fan]], + ['pike'] = [[pike]], + ['story'] = [[cucumber]], + ['plsql'] = [[plsql]], + ['app.src'] = [[erlang]], + ['mkd'] = [[markdown]], + ['ksh'] = [[sh]], + ['inl'] = [[cpp]], + ['ml4'] = [[ocaml]], + ['f77'] = [[fortran]], + ['pls'] = [[plsql]], + ['opencl'] = [[c]], + ['proj'] = [[xml]], + ['4'] = [[groff]], + ['wsf'] = [[xml]], + ['ninja'] = [[ninja]], + ['rest'] = [[rst]], + ['tpb'] = [[plsql]], + ['xquery'] = [[xquery]], + ['gitconfig'] = [[gitconfig]], + ['r3'] = [[rebol]], + ['reb'] = [[rebol]], + ['gawk'] = [[awk]], + ['groovy'] = [[groovy]], + ['auk'] = [[awk]], + ['r'] = [[rebol]], + ['cmd'] = [[dosbatch]], + ['awk'] = [[awk]], + ['xslt'] = [[xslt]], + ['ps1xml'] = [[xml]], + ['spc'] = [[plsql]], + ['wixproj'] = [[xml]], + ['upc'] = [[c]], + ['hic'] = [[clojure]], + ['cljx'] = [[clojure]], + ['cljs'] = [[clojure]], + ['json'] = [[json]], + ['cljc'] = [[clojure]], + ['pfa'] = [[postscr]], + ['vhdl'] = [[vhdl]], + ['cl2'] = [[clojure]], + ['nsi'] = [[nsis]], + ['g4'] = [[antlr]], + ['sage'] = [[python]], + ['haml.deface'] = [[haml]], + ['haml'] = [[haml]], + ['rno'] = [[groff]], + ['xrl'] = [[erlang]], + ['escript'] = [[erlang]], + ['pks'] = [[plsql]], + ['grxml'] = [[xml]], + ['erl'] = [[erlang]], + ['chem'] = [[pic]], + ['eb'] = [[python]], + ['patch'] = [[diff]], + ['diff'] = [[diff]], + ['mspec'] = [[ruby]], + ['xsp-config'] = [[xml]], + ['gv'] = [[dot]], + ['adp'] = [[tcl]], + ['webapp'] = [[json]], + ['epsi'] = [[postscr]], + ['dot'] = [[dot]], + ['phtml'] = [[php]], + ['lid'] = [[dylan]], + ['cpy'] = [[cobol]], + ['mkdn'] = [[markdown]], + ['ccp'] = [[cobol]], + ['cob'] = [[cobol]], + ['glf'] = [[tcl]], + ['sass'] = [[sass]], + ['gnu'] = [[gnuplot]], + ['pyp'] = [[python]], + ['gsp'] = [[gsp]], + ['asn1'] = [[asn]], + ['sig'] = [[sml]], + ['t'] = [[perl]], + ['xlf'] = [[xml]], + ['perl'] = [[perl]], + ['rpy'] = [[python]], + ['jbuilder'] = [[ruby]], + ['mdwn'] = [[markdown]], + ['dfm'] = [[pascal]], + ['c'] = [[c]], + ['pyw'] = [[python]], + ['mkfile'] = [[make]], + ['py3'] = [[python]], + ['gypi'] = [[python]], + ['py'] = [[python]], + ['watchr'] = [[ruby]], + ['thor'] = [[ruby]], + ['ruby'] = [[ruby]], + ['ru'] = [[ruby]], + ['gltf'] = [[json]], + ['rbx'] = [[ruby]], + ['rbuild'] = [[ruby]], + ['m4'] = [[m4]], + ['god'] = [[ruby]], + ['har'] = [[json]], + ['sas'] = [[sas]], + ['mkii'] = [[tex]], + ['frt'] = [[forth]], + ['icl'] = [[clean]], + ['mirah'] = [[ruby]], + ['wxi'] = [[xml]], + ['lektorproject'] = [[dosini]], + ['nasm'] = [[asm]], + ['njs'] = [[javascript]], + ['vstemplate'] = [[xml]], + ['jsm'] = [[javascript]], + ['html'] = [[html]], + ['xpm'] = [[xpm]], + ['tool'] = [[sh]], + ['rdf'] = [[xml]], + ['rd'] = [[r]], + ['dyl'] = [[dylan]], + ['p8'] = [[lua]], + ['prw'] = [[clipper]], + ['es6'] = [[javascript]], + ['gmx'] = [[xml]], + ['pluginspec'] = [[ruby]], + ['gemspec'] = [[ruby]], + ['aux'] = [[tex]], + ['lvlib'] = [[xml]], + ['cats'] = [[c]], + ['fcgi'] = [[perl]], + ['for'] = [[forth]], + ['scxml'] = [[xml]], + ['mdown'] = [[markdown]], + ['scala'] = [[scala]], + ['pxd'] = [[pyrex]], + ['mxml'] = [[xml]], + ['chs'] = [[haskell]], + ['syntax'] = [[yaml]], + ['5'] = [[groff]], + ['xliff'] = [[xml]], + ['pyt'] = [[python]], + ['view.lkml'] = [[yaml]], + ['pat'] = [[json]], + ['bash'] = [[sh]], + ['tfstate'] = [[json]], + ['vmb'] = [[vim]], + ['prolog'] = [[prolog]], + ['asciidoc'] = [[asciidoc]], + ['asset'] = [[yaml]], + ['mxt'] = [[json]], + ['rviz'] = [[yaml]], + ['yaml.sed'] = [[yaml]], + ['inc'] = [[php]], + ['props'] = [[xml]], + ['psgi'] = [[perl]], + ['axml'] = [[xml]], + ['pxi'] = [[pyrex]], + ['admx'] = [[xml]], + ['nse'] = [[lua]], + ['wxs'] = [[xml]], + ['ice'] = [[json]], + ['builder'] = [[ruby]], + ['jsp'] = [[jsp]], + ['eliom'] = [[ocaml]], + ['go'] = [[go]], + ['ihlp'] = [[stata]], + ['scss'] = [[scss]], + ['sce'] = [[scilab]], + ['lsp'] = [[lisp]], + ['x3d'] = [[xml]], + ['rs'] = [[rust]], + ['php'] = [[php]], + ['htm'] = [[html]], + ['pprx'] = [[rexx]], + ['es'] = [[erlang]], + ['js'] = [[javascript]], + ['ts'] = [[typescript]], + ['uno'] = [[cs]], + ['sch'] = [[scheme]], + ['lvproj'] = [[xml]], + ['xs'] = [[xs]], + ['pro'] = [[idl]], + ['dotsettings'] = [[xml]], + ['res'] = [[xml]], + ['xmi'] = [[xml]], + ['ahkl'] = [[autohotkey]], + ['plt'] = [[gnuplot]], + ['wlt'] = [[mma]], + ['lpr'] = [[pascal]], + ['dof'] = [[dosini]], + ['fs'] = [[forth]], + ['sh.in'] = [[sh]], + ['bat'] = [[dosbatch]], + ['roff'] = [[groff]], + ['depproj'] = [[xml]], + ['mdx'] = [[markdown]], + ['hs'] = [[haskell]], + ['frag'] = [[javascript]], + ['ps'] = [[postscr]], + ['9'] = [[groff]], + ['eps'] = [[postscr]], + ['ck'] = [[java]], + ['rst'] = [[rst]], + ['css'] = [[css]], + ['aw'] = [[php]], + ['veo'] = [[verilog]], + ['adml'] = [[xml]], + ['mll'] = [[ocaml]], + ['tst'] = [[scilab]], + ['svg'] = [[svg]], + ['bdy'] = [[plsql]], + ['xql'] = [[xquery]], + ['xqm'] = [[xquery]], + ['pm'] = [[perl]], + ['texinfo'] = [[texinfo]], + ['prc'] = [[plsql]], + ['3in'] = [[groff]], + ['rbxs'] = [[lua]], + ['xproj'] = [[xml]], + ['bdf'] = [[bdf]], + ['ampl'] = [[ampl]], + ['d'] = [[d]], + ['fnc'] = [[plsql]], + ['cobol'] = [[cobol]], + ['txt'] = [[text]], + ['vim'] = [[vim]], + ['mata'] = [[stata]], + ['linq'] = [[cs]], + ['launch'] = [[xml]], + ['sv'] = [[systemverilog]], + ['nlogo'] = [[lisp]], + ['sci'] = [[scilab]], + ['dockerfile'] = [[dockerfile]], + ['vht'] = [[vhdl]], + ['sed'] = [[sed]], + ['xht'] = [[html]], + ['liquid'] = [[liquid]], + ['latte'] = [[latte]], + ['vhost'] = [[apache]], + ['matah'] = [[stata]], + ['ronn'] = [[markdown]], + ['tpl'] = [[smarty]], + ['xhtml'] = [[html]], + ['bones'] = [[javascript]], + ['bats'] = [[sh]], + ['xpl'] = [[xml]], + ['shproj'] = [[xml]], + ['tfstate.backup'] = [[json]], + ['bzl'] = [[bzl]], + ['cpp'] = [[cpp]], + ['cppm'] = [[cpp]], + ['mod'] = [[ampl]], + ['idc'] = [[c]], + ['hh'] = [[cpp]], + ['druby'] = [[ruby]], + ['apacheconf'] = [[apache]], + ['l'] = [[lex]], + ['md'] = [[markdown]], + ['vxml'] = [[xml]], + ['pmod'] = [[pike]], + ['wsdl'] = [[xml]], + ['mtml'] = [[basic]], + ['vh'] = [[systemverilog]], + ['ddl'] = [[plsql]], + ['topojson'] = [[json]], + ['mysql'] = [[sql]], + ['kojo'] = [[scala]], + ['rsx'] = [[r]], + ['tml'] = [[xml]], + ['dcl'] = [[clean]], + ['reek'] = [[yaml]], + ['yaml'] = [[yaml]], + ['desktop'] = [[desktop]], + ['tsx'] = [[xml]], + }, + file_name = { + ['.classpath'] = [[xml]], + ['bsdmakefile'] = [[make]], + ['delete.me'] = [[text]], + ['packages.config'] = [[xml]], + ['jenkinsfile'] = [[groovy]], + ['ant.xml'] = [[ant]], + ['makefile.frag'] = [[make]], + ['puppetfile'] = [[ruby]], + ['.inputrc'] = [[readline]], + ['.zshrc'] = [[sh]], + ['inputrc'] = [[readline]], + ['gnumakefile'] = [[make]], + ['makefile.sco'] = [[make]], + ['pkgbuild'] = [[sh]], + ['.babelignore'] = [[gitignore]], + ['.bash_logout'] = [[sh]], + ['.nanorc'] = [[nanorc]], + ['berksfile'] = [[ruby]], + ['lexer.x'] = [[lex]], + ['sconscript'] = [[python]], + ['makefile.boot'] = [[make]], + ['starfield'] = [[tcl]], + ['.login'] = [[sh]], + ['composer.lock'] = [[json]], + ['dir_colors'] = [[dircolors]], + ['.nvimrc'] = [[vim]], + ['.project'] = [[xml]], + ['web.config'] = [[xml]], + ['makefile.in'] = [[make]], + ['readme.me'] = [[text]], + ['.cproject'] = [[xml]], + ['nuget.config'] = [[xml]], + ['zlogin'] = [[sh]], + ['phakefile'] = [[php]], + ['brewfile'] = [[ruby]], + ['podfile'] = [[ruby]], + ['use.stable.mask'] = [[text]], + ['.gemrc'] = [[yaml]], + ['emakefile'] = [[erlang]], + ['notebook'] = [[json]], + ['read.me'] = [[text]], + ['.dircolors'] = [[dircolors]], + ['.gitmodules'] = [[gitconfig]], + ['owh'] = [[tcl]], + ['zlogout'] = [[sh]], + ['package.use.stable.mask'] = [[text]], + ['httpd.conf'] = [[apache]], + ['buildozer.spec'] = [[dosini]], + ['.zshenv'] = [[sh]], + ['workspace'] = [[bzl]], + ['readme.1st'] = [[text]], + ['.simplecov'] = [[ruby]], + ['.vimrc'] = [[vim]], + ['.htmlhintrc'] = [[json]], + ['mmn'] = [[groff]], + ['rexfile'] = [[perl]], + ['m3overrides'] = [[quake]], + ['.arcconfig'] = [[json]], + ['.htaccess'] = [[apache]], + ['.php_cs'] = [[php]], + ['tiltfile'] = [[bzl]], + ['wscript'] = [[python]], + ['.stylelintignore'] = [[gitignore]], + ['gvimrc'] = [[vim]], + ['.tern-config'] = [[json]], + ['_dir_colors'] = [[dircolors]], + ['app.config'] = [[xml]], + ['abbrev_defs'] = [[lisp]], + ['_emacs'] = [[lisp]], + ['buck'] = [[bzl]], + ['dockerfile'] = [[dockerfile]], + ['project.ede'] = [[lisp]], + ['thorfile'] = [[ruby]], + ['rakefile'] = [[ruby]], + ['.viper'] = [[lisp]], + ['.spacemacs'] = [[lisp]], + ['.gnus'] = [[lisp]], + ['snapfile'] = [[ruby]], + ['eqnrc'] = [[groff]], + ['_dircolors'] = [[dircolors]], + ['configure.ac'] = [[m4]], + ['cabal.project'] = [[cabal]], + ['.env'] = [[sh]], + ['.luacheckrc'] = [[lua]], + ['9fs'] = [[sh]], + ['cabal.config'] = [[cabal]], + ['.bash_aliases'] = [[sh]], + ['install.mysql'] = [[text]], + ['yarn.lock'] = [[yaml]], + ['gitignore_global'] = [[gitignore]], + ['.cshrc'] = [[sh]], + ['.bash_history'] = [[sh]], + ['.env.example'] = [[sh]], + ['.npmignore'] = [[gitignore]], + ['gitignore-global'] = [[gitignore]], + ['build.bazel'] = [[bzl]], + ['.flaskenv'] = [[sh]], + ['license'] = [[text]], + ['mavenfile'] = [[ruby]], + ['.vscodeignore'] = [[gitignore]], + ['.abbrev_defs'] = [[lisp]], + ['gemfile.lock'] = [[ruby]], + ['go.mod'] = [[text]], + ['.gvimrc'] = [[vim]], + ['.nodemonignore'] = [[gitignore]], + ['bashrc'] = [[sh]], + ['man'] = [[sh]], + ['.dockerignore'] = [[gitignore]], + ['makefile.am'] = [[make]], + ['.zlogin'] = [[sh]], + ['.cvsignore'] = [[gitignore]], + ['.coffeelintignore'] = [[gitignore]], + ['.php'] = [[php]], + ['expr-dist'] = [[r]], + ['zshrc'] = [[sh]], + ['.clang-format'] = [[yaml]], + ['bash_aliases'] = [[sh]], + ['.exrc'] = [[vim]], + ['web.release.config'] = [[xml]], + ['.atomignore'] = [[gitignore]], + ['makefile.wat'] = [[make]], + ['troffrc-end'] = [[groff]], + ['vimrc'] = [[vim]], + ['cmakelists.txt'] = [[cmake]], + ['.gitconfig'] = [[gitconfig]], + ['riemann.config'] = [[clojure]], + ['zprofile'] = [[sh]], + ['rebar.lock'] = [[erlang]], + ['news'] = [[text]], + ['rebar.config'] = [[erlang]], + ['mcmod.info'] = [[json]], + ['cpanfile'] = [[perl]], + ['readme.mysql'] = [[text]], + ['makefile.pl'] = [[perl]], + ['dangerfile'] = [[ruby]], + ['snakefile'] = [[python]], + ['contents.lr'] = [[markdown]], + ['sconstruct'] = [[python]], + ['glide.lock'] = [[yaml]], + ['deps'] = [[python]], + ['.zprofile'] = [[sh]], + ['.gclient'] = [[python]], + ['keep.me'] = [[text]], + ['troffrc'] = [[groff]], + ['m3makefile'] = [[quake]], + ['cshrc'] = [[sh]], + ['.emacs.desktop'] = [[lisp]], + ['.zlogout'] = [[sh]], + ['.rprofile'] = [[r]], + ['cask'] = [[lisp]], + ['jarfile'] = [[ruby]], + ['deliverfile'] = [[ruby]], + ['copyright.regex'] = [[text]], + ['.prettierignore'] = [[gitignore]], + ['gemfile'] = [[ruby]], + ['profile'] = [[sh]], + ['fastfile'] = [[ruby]], + ['guardfile'] = [[ruby]], + ['capfile'] = [[ruby]], + ['buildfile'] = [[ruby]], + ['jakefile'] = [[javascript]], + ['appraisals'] = [[ruby]], + ['fontlog'] = [[text]], + ['package.use.mask'] = [[text]], + ['.pryrc'] = [[ruby]], + ['.emacs'] = [[lisp]], + ['.watchmanconfig'] = [[json]], + ['.irbrc'] = [[ruby]], + ['.tern-project'] = [[json]], + ['web.debug.config'] = [[xml]], + ['.eslintignore'] = [[gitignore]], + ['.gitignore'] = [[gitignore]], + ['copying.regex'] = [[text]], + ['build'] = [[bzl]], + ['nvimrc'] = [[vim]], + ['.dir_colors'] = [[dircolors]], + ['.bashrc'] = [[sh]], + ['kbuild'] = [[make]], + ['.profile'] = [[sh]], + ['.php_cs.dist'] = [[php]], + ['gradlew'] = [[sh]], + ['settings.stylecop'] = [[xml]], + ['makefile.inc'] = [[make]], + ['mkfile'] = [[make]], + ['.bzrignore'] = [[gitignore]], + ['ack'] = [[perl]], + ['license.mysql'] = [[text]], + ['makefile'] = [[make]], + ['vagrantfile'] = [[ruby]], + ['nanorc'] = [[nanorc]], + ['.bash_profile'] = [[sh]], + ['bash_logout'] = [[sh]], + ['bash_profile'] = [[sh]], + ['click.me'] = [[text]], + ['login'] = [[sh]], + ['_vimrc'] = [[vim]], + ['go.sum'] = [[text]], + ['apache2.conf'] = [[apache]], + ['use.mask'] = [[text]], + ['build.xml'] = [[ant]], + ['mmt'] = [[groff]], + ['zshenv'] = [[sh]], + ['copying'] = [[text]], + ['install'] = [[text]], + ['test.me'] = [[text]], + ['package.mask'] = [[text]], + ['.clang-tidy'] = [[yaml]], + ['readme.nss'] = [[text]], + ['rebar.config.lock'] = [[erlang]], + }, +} diff --git a/.config/nvim/pack/tree/start/plenary.nvim/data/plenary/filetypes/builtin.lua b/.config/nvim/pack/tree/start/plenary.nvim/data/plenary/filetypes/builtin.lua new file mode 100644 index 0000000..6f341d6 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/data/plenary/filetypes/builtin.lua @@ -0,0 +1,73 @@ +local shebang_prefixes = { '/usr/bin/', '/bin/', '/usr/bin/env ', '/bin/env ' } +local shebang_fts = { + ['fish'] = 'fish', + ['perl'] = 'perl', + ['python'] = 'python', + ['python2'] = 'python', + ['python3'] = 'python', + ['bash'] = 'sh', + ['sh'] = 'sh', + ['zsh'] = 'zsh', +} + +local shebang = {} +for _, prefix in ipairs(shebang_prefixes) do + for k, v in pairs(shebang_fts) do + shebang[prefix .. k] = v + end +end + +return { + extension = { + ['_coffee'] = 'coffee', + ['astro'] = 'astro', + ['cairo'] = 'cairo', + ['cts'] = 'typescript', + ['cljd'] = 'clojure', + ['coffee'] = 'coffee', + ['dart'] = 'dart', + ['erb'] = 'eruby', + ['ex'] = 'elixir', + ['exs'] = 'elixir', + ['fish'] = 'fish', + ['fnl'] = 'fennel', + ['gd'] = 'gdscript', + ['gql'] = 'graphql', + ['gradle'] = 'groovy', + ['graphql'] = 'graphql', + ['hbs'] = 'handlebars', + ['hdbs'] = 'handlebars', + ['hlsl'] = 'hlsl', + ['jai'] = 'jai', + ['janet'] = 'janet', + ['jl'] = 'julia', + ['jsx'] = 'javascriptreact', + ['kt'] = 'kotlin', + ['mts'] = 'typescript', + ['nix'] = 'nix', + ['plist'] = 'xml', + ['purs'] = 'purescript', + ['r'] = 'r', + ['res'] = 'rescript', + ['resi'] = 'rescript', + ['rkt'] = 'racket', + ['svelte'] = 'svelte', + ['swift'] = 'swift', + ['tres'] = 'gdresource', + ['tscn'] = 'gdresource', + ['tsx'] = 'typescriptreact', + ['smithy'] = [[smithy]], + ['sol'] = 'solidity', + ['dtsi'] = 'dts', + }, + file_name = { + ['cakefile'] = 'coffee', + ['.babelrc'] = 'json', + ['.clangd'] = 'yaml', + ['.eslintrc'] = 'json', + ['.firebaserc'] = 'json', + ['.prettierrc'] = 'json', + ['.stylelintrc'] = 'json', + }, + shebang = shebang +} diff --git a/.config/nvim/pack/tree/start/plenary.nvim/doc/plenary-test.txt b/.config/nvim/pack/tree/start/plenary.nvim/doc/plenary-test.txt new file mode 100644 index 0000000..d1c7373 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/doc/plenary-test.txt @@ -0,0 +1,110 @@ +*plenary-test* + + +Supports (simple) busted-style testing. It implements a mock-ed busted +interface, that will allow you to run simple busted style tests in separate +neovim instances. + + + +USAGE *plenary-test-usage* +============================================================================== + +To run the current spec file in a floating window, you can use the keymap +`PlenaryTestFile`. For example: +> + nmap t PlenaryTestFile +< +In this case, the test is run with a minimal configuration, that includes in +its runtimepath only plenary.nvim and the current working directory. + +To run a whole directory from the command line, you could do something like: +> + nvim --headless -c "PlenaryBustedDirectory tests/plenary/ { }" + +Where the first argument is the directory you'd like to test. It will search +for files with the pattern `*_spec.lua` and execute them in separate neovim +instances. + +The second argument is a Lua option table with the following fields: + + `nvim_cmd` specify the command to launch this neovim instance (defaults + to `vim.v.progpath`). + `init` specify an init.vim to use for this instance, if not given + a minimal configuration is used. + `minimal_init` as for `init`, but also run the neovim instance with + `--noplugin`. + `sequential` whether to run tests sequentially (default is to run in + parallel). + `keep_going` if `sequential`, whether to continue on test failure (default + true). + `timeout` controls the maximum time allotted to each job in parallel or + sequential operation (defaults to 50,000 milliseconds). + +Unless `init` is given, the neovim instance is run with the `--noplugin` +argument. + +The exit code is 0 for success and 1 for fail, so you can use it easily in +a `Makefile`. + + + +SUPPORTED BUSTED ITEMS *plenary-test-busted* +============================================================================== + +So far, the only supported busted items are: + +- `describe` +- `it` +- `pending` +- `before_each` +- `after_each` +- `clear` +- `assert.*` etc. (from luassert, which is bundled) + +We used to support `luaunit` and original `busted` but it turns out it was way +too hard and not worthwhile for the difficulty of getting them setup, +particularly on other platforms or in CI. Now, we have a dep free (or at +least, no other installation steps necessary) `busted` implementation that can +be used more easily. + +Please take a look at the new APIs and make any issues for things that aren't +clear. + + + +COMMANDS *plenary-test-commands* +============================================================================== + +*:PlenaryBustedFile* {path} + + Run a test on a single `_spec.lua` file. + + +*:PlenaryBustedDirectory* {path} {options} + + Run tests for all `*_spec.lua` files in the given path. + + {options} is a table, see |plenary-test-usage|. + + + + +PLUGS *plenary-test-plugs* +============================================================================== + +PlenaryTestFile + + Can be used to run a test on a single file, with a minimal configuration. + + + + +LICENSE *plenary-test-license* +============================================================================== + +MIT license + + +============================================================================== +vim:tw=78:ft=help:norl:et:ts=2:sw=2:fen:fdl=0: diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/array.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/array.lua new file mode 100644 index 0000000..f9cb655 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/array.lua @@ -0,0 +1,70 @@ +local assert = require('luassert.assert') +local say = require('say') + +-- Example usage: +-- local arr = { "one", "two", "three" } +-- +-- assert.array(arr).has.no.holes() -- checks the array to not contain holes --> passes +-- assert.array(arr).has.no.holes(4) -- sets explicit length to 4 --> fails +-- +-- local first_hole = assert.array(arr).has.holes(4) -- check array of size 4 to contain holes --> passes +-- assert.equal(4, first_hole) -- passes, as the index of the first hole is returned + + +-- Unique key to store the object we operate on in the state object +-- key must be unique, to make sure we do not have name collissions in the shared state object +local ARRAY_STATE_KEY = "__array_state" + +-- The modifier, to store the object in our state +local function array(state, args, level) + assert(args.n > 0, "No array provided to the array-modifier") + assert(rawget(state, ARRAY_STATE_KEY) == nil, "Array already set") + rawset(state, ARRAY_STATE_KEY, args[1]) + return state +end + +-- The actual assertion that operates on our object, stored via the modifier +local function holes(state, args, level) + local length = args[1] + local arr = rawget(state, ARRAY_STATE_KEY) -- retrieve previously set object + -- only check against nil, metatable types are allowed + assert(arr ~= nil, "No array set, please use the array modifier to set the array to validate") + if length == nil then + length = 0 + for i in pairs(arr) do + if type(i) == "number" and + i > length and + math.floor(i) == i then + length = i + end + end + end + assert(type(length) == "number", "expected array length to be of type 'number', got: "..tostring(length)) + -- let's do the actual assertion + local missing + for i = 1, length do + if arr[i] == nil then + missing = i + break + end + end + -- format arguments for output strings; + args[1] = missing + args.n = missing and 1 or 0 + return missing ~= nil, { missing } -- assert result + first missing index as return value +end + +-- Register the proper assertion messages +say:set("assertion.array_holes.positive", [[ +Expected array to have holes, but none was found. +]]) +say:set("assertion.array_holes.negative", [[ +Expected array to not have holes, hole found at position: %s +]]) + +-- Register the assertion, and the modifier +assert:register("assertion", "holes", holes, + "assertion.array_holes.positive", + "assertion.array_holes.negative") + +assert:register("modifier", "array", array) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/assert.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/assert.lua new file mode 100644 index 0000000..7fe7569 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/assert.lua @@ -0,0 +1,180 @@ +local s = require 'say' +local astate = require 'luassert.state' +local util = require 'luassert.util' +local unpack = util.unpack +local obj -- the returned module table +local level_mt = {} + +-- list of namespaces +local namespace = require 'luassert.namespaces' + +local function geterror(assertion_message, failure_message, args) + if util.hastostring(failure_message) then + failure_message = tostring(failure_message) + elseif failure_message ~= nil then + failure_message = astate.format_argument(failure_message) + end + local message = s(assertion_message, obj:format(args)) + if message and failure_message then + message = failure_message .. "\n" .. message + end + return message or failure_message +end + +local __state_meta = { + + __call = function(self, ...) + local keys = util.extract_keys("assertion", self.tokens) + + local assertion + + for _, key in ipairs(keys) do + assertion = namespace.assertion[key] or assertion + end + + if assertion then + for _, key in ipairs(keys) do + if namespace.modifier[key] then + namespace.modifier[key].callback(self) + end + end + + local arguments = util.make_arglist(...) + local val, retargs = assertion.callback(self, arguments, util.errorlevel()) + + if (not val) == self.mod then + local message = assertion.positive_message + if not self.mod then + message = assertion.negative_message + end + local err = geterror(message, rawget(self,"failure_message"), arguments) + error(err or "assertion failed!", util.errorlevel()) + end + + if retargs then + return unpack(retargs) + end + return ... + else + local arguments = util.make_arglist(...) + self.tokens = {} + + for _, key in ipairs(keys) do + if namespace.modifier[key] then + namespace.modifier[key].callback(self, arguments, util.errorlevel()) + end + end + end + + return self + end, + + __index = function(self, key) + for token in key:lower():gmatch('[^_]+') do + table.insert(self.tokens, token) + end + + return self + end +} + +obj = { + state = function() return setmetatable({mod=true, tokens={}}, __state_meta) end, + + -- registers a function in namespace + register = function(self, nspace, name, callback, positive_message, negative_message) + local lowername = name:lower() + if not namespace[nspace] then + namespace[nspace] = {} + end + namespace[nspace][lowername] = { + callback = callback, + name = lowername, + positive_message=positive_message, + negative_message=negative_message + } + end, + + -- unregisters a function in a namespace + unregister = function(self, nspace, name) + local lowername = name:lower() + if not namespace[nspace] then + namespace[nspace] = {} + end + namespace[nspace][lowername] = nil + end, + + -- registers a formatter + -- a formatter takes a single argument, and converts it to a string, or returns nil if it cannot format the argument + add_formatter = function(self, callback) + astate.add_formatter(callback) + end, + + -- unregisters a formatter + remove_formatter = function(self, fmtr) + astate.remove_formatter(fmtr) + end, + + format = function(self, args) + -- args.n specifies the number of arguments in case of 'trailing nil' arguments which get lost + local nofmt = args.nofmt or {} -- arguments in this list should not be formatted + local fmtargs = args.fmtargs or {} -- additional arguments to be passed to formatter + for i = 1, (args.n or #args) do -- cannot use pairs because table might have nils + if not nofmt[i] then + local val = args[i] + local valfmt = astate.format_argument(val, nil, fmtargs[i]) + if valfmt == nil then valfmt = tostring(val) end -- no formatter found + args[i] = valfmt + end + end + return args + end, + + set_parameter = function(self, name, value) + astate.set_parameter(name, value) + end, + + get_parameter = function(self, name) + return astate.get_parameter(name) + end, + + add_spy = function(self, spy) + astate.add_spy(spy) + end, + + snapshot = function(self) + return astate.snapshot() + end, + + level = function(self, level) + return setmetatable({ + level = level + }, level_mt) + end, + + -- returns the level if a level-value, otherwise nil + get_level = function(self, level) + if getmetatable(level) ~= level_mt then + return nil -- not a valid error-level + end + return level.level + end, +} + +local __meta = { + + __call = function(self, bool, message, level, ...) + if not bool then + local err_level = (self:get_level(level) or 1) + 1 + error(message or "assertion failed!", err_level) + end + return bool , message , level , ... + end, + + __index = function(self, key) + return rawget(self, key) or self.state()[key] + end, + +} + +return setmetatable(obj, __meta) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/assertions.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/assertions.lua new file mode 100644 index 0000000..e5083c3 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/assertions.lua @@ -0,0 +1,334 @@ +-- module will not return anything, only register assertions with the main assert engine + +-- assertions take 2 parameters; +-- 1) state +-- 2) arguments list. The list has a member 'n' with the argument count to check for trailing nils +-- 3) level The level of the error position relative to the called function +-- returns; boolean; whether assertion passed + +local assert = require('luassert.assert') +local astate = require ('luassert.state') +local util = require ('luassert.util') +local s = require('say') + +local function format(val) + return astate.format_argument(val) or tostring(val) +end + +local function set_failure_message(state, message) + if message ~= nil then + state.failure_message = message + end +end + +local function unique(state, arguments, level) + local list = arguments[1] + local deep + local argcnt = arguments.n + if type(arguments[2]) == "boolean" or (arguments[2] == nil and argcnt > 2) then + deep = arguments[2] + set_failure_message(state, arguments[3]) + else + if type(arguments[3]) == "boolean" then + deep = arguments[3] + end + set_failure_message(state, arguments[2]) + end + for k,v in pairs(list) do + for k2, v2 in pairs(list) do + if k ~= k2 then + if deep and util.deepcompare(v, v2, true) then + return false + else + if v == v2 then + return false + end + end + end + end + end + return true +end + +local function near(state, arguments, level) + local level = (level or 1) + 1 + local argcnt = arguments.n + assert(argcnt > 2, s("assertion.internal.argtolittle", { "near", 3, tostring(argcnt) }), level) + local expected = tonumber(arguments[1]) + local actual = tonumber(arguments[2]) + local tolerance = tonumber(arguments[3]) + local numbertype = "number or object convertible to a number" + assert(expected, s("assertion.internal.badargtype", { 1, "near", numbertype, format(arguments[1]) }), level) + assert(actual, s("assertion.internal.badargtype", { 2, "near", numbertype, format(arguments[2]) }), level) + assert(tolerance, s("assertion.internal.badargtype", { 3, "near", numbertype, format(arguments[3]) }), level) + -- switch arguments for proper output message + util.tinsert(arguments, 1, util.tremove(arguments, 2)) + arguments[3] = tolerance + arguments.nofmt = arguments.nofmt or {} + arguments.nofmt[3] = true + set_failure_message(state, arguments[4]) + return (actual >= expected - tolerance and actual <= expected + tolerance) +end + +local function matches(state, arguments, level) + local level = (level or 1) + 1 + local argcnt = arguments.n + assert(argcnt > 1, s("assertion.internal.argtolittle", { "matches", 2, tostring(argcnt) }), level) + local pattern = arguments[1] + local actual = nil + if util.hastostring(arguments[2]) or type(arguments[2]) == "number" then + actual = tostring(arguments[2]) + end + local err_message + local init_arg_num = 3 + for i=3,argcnt,1 do + if arguments[i] and type(arguments[i]) ~= "boolean" and not tonumber(arguments[i]) then + if i == 3 then init_arg_num = init_arg_num + 1 end + err_message = util.tremove(arguments, i) + break + end + end + local init = arguments[3] + local plain = arguments[4] + local stringtype = "string or object convertible to a string" + assert(type(pattern) == "string", s("assertion.internal.badargtype", { 1, "matches", "string", type(arguments[1]) }), level) + assert(actual, s("assertion.internal.badargtype", { 2, "matches", stringtype, format(arguments[2]) }), level) + assert(init == nil or tonumber(init), s("assertion.internal.badargtype", { init_arg_num, "matches", "number", type(arguments[3]) }), level) + -- switch arguments for proper output message + util.tinsert(arguments, 1, util.tremove(arguments, 2)) + set_failure_message(state, err_message) + local retargs + local ok + if plain then + ok = (actual:find(pattern, init, plain) ~= nil) + retargs = ok and { pattern } or {} + else + retargs = { actual:match(pattern, init) } + ok = (retargs[1] ~= nil) + end + return ok, retargs +end + +local function equals(state, arguments, level) + local level = (level or 1) + 1 + local argcnt = arguments.n + assert(argcnt > 1, s("assertion.internal.argtolittle", { "equals", 2, tostring(argcnt) }), level) + local result = arguments[1] == arguments[2] + -- switch arguments for proper output message + util.tinsert(arguments, 1, util.tremove(arguments, 2)) + set_failure_message(state, arguments[3]) + return result +end + +local function same(state, arguments, level) + local level = (level or 1) + 1 + local argcnt = arguments.n + assert(argcnt > 1, s("assertion.internal.argtolittle", { "same", 2, tostring(argcnt) }), level) + if type(arguments[1]) == 'table' and type(arguments[2]) == 'table' then + local result, crumbs = util.deepcompare(arguments[1], arguments[2], true) + -- switch arguments for proper output message + util.tinsert(arguments, 1, util.tremove(arguments, 2)) + arguments.fmtargs = arguments.fmtargs or {} + arguments.fmtargs[1] = { crumbs = crumbs } + arguments.fmtargs[2] = { crumbs = crumbs } + set_failure_message(state, arguments[3]) + return result + end + local result = arguments[1] == arguments[2] + -- switch arguments for proper output message + util.tinsert(arguments, 1, util.tremove(arguments, 2)) + set_failure_message(state, arguments[3]) + return result +end + +local function truthy(state, arguments, level) + local argcnt = arguments.n + assert(argcnt > 0, s("assertion.internal.argtolittle", { "truthy", 1, tostring(argcnt) }), level) + set_failure_message(state, arguments[2]) + return arguments[1] ~= false and arguments[1] ~= nil +end + +local function falsy(state, arguments, level) + local argcnt = arguments.n + assert(argcnt > 0, s("assertion.internal.argtolittle", { "falsy", 1, tostring(argcnt) }), level) + return not truthy(state, arguments, level) +end + +local function has_error(state, arguments, level) + local level = (level or 1) + 1 + local retargs = util.shallowcopy(arguments) + local func = arguments[1] + local err_expected = arguments[2] + local failure_message = arguments[3] + assert(util.callable(func), s("assertion.internal.badargtype", { 1, "error", "function or callable object", type(func) }), level) + local ok, err_actual = pcall(func) + if type(err_actual) == 'string' then + -- remove 'path/to/file:line: ' from string + err_actual = err_actual:gsub('^.-:%d+: ', '', 1) + end + retargs[1] = err_actual + arguments.nofmt = {} + arguments.n = 2 + arguments[1] = (ok and '(no error)' or err_actual) + arguments[2] = (err_expected == nil and '(error)' or err_expected) + arguments.nofmt[1] = ok + arguments.nofmt[2] = (err_expected == nil) + set_failure_message(state, failure_message) + + if ok or err_expected == nil then + return not ok, retargs + end + if type(err_expected) == 'string' then + -- err_actual must be (convertible to) a string + if util.hastostring(err_actual) then + err_actual = tostring(err_actual) + retargs[1] = err_actual + end + if type(err_actual) == 'string' then + return err_expected == err_actual, retargs + end + elseif type(err_expected) == 'number' then + if type(err_actual) == 'string' then + return tostring(err_expected) == tostring(tonumber(err_actual)), retargs + end + end + return same(state, {err_expected, err_actual, ["n"] = 2}), retargs +end + +local function error_matches(state, arguments, level) + local level = (level or 1) + 1 + local retargs = util.shallowcopy(arguments) + local argcnt = arguments.n + local func = arguments[1] + local pattern = arguments[2] + assert(argcnt > 1, s("assertion.internal.argtolittle", { "error_matches", 2, tostring(argcnt) }), level) + assert(util.callable(func), s("assertion.internal.badargtype", { 1, "error_matches", "function or callable object", type(func) }), level) + assert(pattern == nil or type(pattern) == "string", s("assertion.internal.badargtype", { 2, "error", "string", type(pattern) }), level) + + local failure_message + local init_arg_num = 3 + for i=3,argcnt,1 do + if arguments[i] and type(arguments[i]) ~= "boolean" and not tonumber(arguments[i]) then + if i == 3 then init_arg_num = init_arg_num + 1 end + failure_message = util.tremove(arguments, i) + break + end + end + local init = arguments[3] + local plain = arguments[4] + assert(init == nil or tonumber(init), s("assertion.internal.badargtype", { init_arg_num, "matches", "number", type(arguments[3]) }), level) + + local ok, err_actual = pcall(func) + if type(err_actual) == 'string' then + -- remove 'path/to/file:line: ' from string + err_actual = err_actual:gsub('^.-:%d+: ', '', 1) + end + retargs[1] = err_actual + arguments.nofmt = {} + arguments.n = 2 + arguments[1] = (ok and '(no error)' or err_actual) + arguments[2] = pattern + arguments.nofmt[1] = ok + arguments.nofmt[2] = false + set_failure_message(state, failure_message) + + if ok then return not ok, retargs end + if err_actual == nil and pattern == nil then + return true, {} + end + + -- err_actual must be (convertible to) a string + if util.hastostring(err_actual) or + type(err_actual) == "number" or + type(err_actual) == "boolean" then + err_actual = tostring(err_actual) + retargs[1] = err_actual + end + if type(err_actual) == 'string' then + local ok + local retargs_ok + if plain then + retargs_ok = { pattern } + ok = (err_actual:find(pattern, init, plain) ~= nil) + else + retargs_ok = { err_actual:match(pattern, init) } + ok = (retargs_ok[1] ~= nil) + end + if ok then retargs = retargs_ok end + return ok, retargs + end + + return false, retargs +end + +local function is_true(state, arguments, level) + util.tinsert(arguments, 2, true) + set_failure_message(state, arguments[3]) + return arguments[1] == arguments[2] +end + +local function is_false(state, arguments, level) + util.tinsert(arguments, 2, false) + set_failure_message(state, arguments[3]) + return arguments[1] == arguments[2] +end + +local function is_type(state, arguments, level, etype) + util.tinsert(arguments, 2, "type " .. etype) + arguments.nofmt = arguments.nofmt or {} + arguments.nofmt[2] = true + set_failure_message(state, arguments[3]) + return arguments.n > 1 and type(arguments[1]) == etype +end + +local function returned_arguments(state, arguments, level) + arguments[1] = tostring(arguments[1]) + arguments[2] = tostring(arguments.n - 1) + arguments.nofmt = arguments.nofmt or {} + arguments.nofmt[1] = true + arguments.nofmt[2] = true + if arguments.n < 2 then arguments.n = 2 end + return arguments[1] == arguments[2] +end + +local function set_message(state, arguments, level) + state.failure_message = arguments[1] +end + +local function is_boolean(state, arguments, level) return is_type(state, arguments, level, "boolean") end +local function is_number(state, arguments, level) return is_type(state, arguments, level, "number") end +local function is_string(state, arguments, level) return is_type(state, arguments, level, "string") end +local function is_table(state, arguments, level) return is_type(state, arguments, level, "table") end +local function is_nil(state, arguments, level) return is_type(state, arguments, level, "nil") end +local function is_userdata(state, arguments, level) return is_type(state, arguments, level, "userdata") end +local function is_function(state, arguments, level) return is_type(state, arguments, level, "function") end +local function is_thread(state, arguments, level) return is_type(state, arguments, level, "thread") end + +assert:register("modifier", "message", set_message) +assert:register("assertion", "true", is_true, "assertion.same.positive", "assertion.same.negative") +assert:register("assertion", "false", is_false, "assertion.same.positive", "assertion.same.negative") +assert:register("assertion", "boolean", is_boolean, "assertion.same.positive", "assertion.same.negative") +assert:register("assertion", "number", is_number, "assertion.same.positive", "assertion.same.negative") +assert:register("assertion", "string", is_string, "assertion.same.positive", "assertion.same.negative") +assert:register("assertion", "table", is_table, "assertion.same.positive", "assertion.same.negative") +assert:register("assertion", "nil", is_nil, "assertion.same.positive", "assertion.same.negative") +assert:register("assertion", "userdata", is_userdata, "assertion.same.positive", "assertion.same.negative") +assert:register("assertion", "function", is_function, "assertion.same.positive", "assertion.same.negative") +assert:register("assertion", "thread", is_thread, "assertion.same.positive", "assertion.same.negative") +assert:register("assertion", "returned_arguments", returned_arguments, "assertion.returned_arguments.positive", "assertion.returned_arguments.negative") + +assert:register("assertion", "same", same, "assertion.same.positive", "assertion.same.negative") +assert:register("assertion", "matches", matches, "assertion.matches.positive", "assertion.matches.negative") +assert:register("assertion", "match", matches, "assertion.matches.positive", "assertion.matches.negative") +assert:register("assertion", "near", near, "assertion.near.positive", "assertion.near.negative") +assert:register("assertion", "equals", equals, "assertion.equals.positive", "assertion.equals.negative") +assert:register("assertion", "equal", equals, "assertion.equals.positive", "assertion.equals.negative") +assert:register("assertion", "unique", unique, "assertion.unique.positive", "assertion.unique.negative") +assert:register("assertion", "error", has_error, "assertion.error.positive", "assertion.error.negative") +assert:register("assertion", "errors", has_error, "assertion.error.positive", "assertion.error.negative") +assert:register("assertion", "error_matches", error_matches, "assertion.error.positive", "assertion.error.negative") +assert:register("assertion", "error_match", error_matches, "assertion.error.positive", "assertion.error.negative") +assert:register("assertion", "matches_error", error_matches, "assertion.error.positive", "assertion.error.negative") +assert:register("assertion", "match_error", error_matches, "assertion.error.positive", "assertion.error.negative") +assert:register("assertion", "truthy", truthy, "assertion.truthy.positive", "assertion.truthy.negative") +assert:register("assertion", "falsy", falsy, "assertion.falsy.positive", "assertion.falsy.negative") diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/compatibility.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/compatibility.lua new file mode 100644 index 0000000..88290ad --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/compatibility.lua @@ -0,0 +1,9 @@ +-- no longer needed, only for backward compatibility +local unpack = require ("luassert.util").unpack + +return { + unpack = function(...) + print(debug.traceback("WARN: calling deprecated function 'luassert.compatibility.unpack' use 'luassert.util.unpack' instead")) + return unpack(...) + end +} diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/formatters/binarystring.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/formatters/binarystring.lua new file mode 100644 index 0000000..02c05ea --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/formatters/binarystring.lua @@ -0,0 +1,28 @@ +local format = function (str) + if type(str) ~= "string" then return nil end + local result = "Binary string length; " .. tostring(#str) .. " bytes\n" + local i = 1 + local hex = "" + local chr = "" + while i <= #str do + local byte = str:byte(i) + hex = string.format("%s%2x ", hex, byte) + if byte < 32 then byte = string.byte(".") end + chr = chr .. string.char(byte) + if math.floor(i/16) == i/16 or i == #str then + -- reached end of line + hex = hex .. string.rep(" ", 16 * 3 - #hex) + chr = chr .. string.rep(" ", 16 - #chr) + + result = result .. hex:sub(1, 8 * 3) .. " " .. hex:sub(8*3+1, -1) .. " " .. chr:sub(1,8) .. " " .. chr:sub(9,-1) .. "\n" + + hex = "" + chr = "" + end + i = i + 1 + end + return result +end + +return format + diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/formatters/init.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/formatters/init.lua new file mode 100644 index 0000000..0ff67c9 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/formatters/init.lua @@ -0,0 +1,255 @@ +-- module will not return anything, only register formatters with the main assert engine +local assert = require('luassert.assert') +local match = require('luassert.match') +local util = require('luassert.util') + +local isatty, colors do + local ok, term = pcall(require, 'term') + isatty = io.type(io.stdout) == 'file' and ok and term.isatty(io.stdout) + if not isatty then + local isWindows = package.config:sub(1,1) == '\\' + if isWindows and os.getenv("ANSICON") then + isatty = true + end + end + + colors = setmetatable({ + none = function(c) return c end + },{ __index = function(self, key) + return function(c) + for token in key:gmatch("[^%.]+") do + c = term.colors[token](c) + end + return c + end + end + }) +end + +local function fmt_string(arg) + if type(arg) == "string" then + return string.format("(string) '%s'", arg) + end +end + +-- A version of tostring which formats numbers more precisely. +local function tostr(arg) + if type(arg) ~= "number" then + return tostring(arg) + end + + if arg ~= arg then + return "NaN" + elseif arg == 1/0 then + return "Inf" + elseif arg == -1/0 then + return "-Inf" + end + + local str = string.format("%.20g", arg) + + if math.type and math.type(arg) == "float" and not str:find("[%.,]") then + -- Number is a float but looks like an integer. + -- Insert ".0" after first run of digits. + str = str:gsub("%d+", "%0.0", 1) + end + + return str +end + +local function fmt_number(arg) + if type(arg) == "number" then + return string.format("(number) %s", tostr(arg)) + end +end + +local function fmt_boolean(arg) + if type(arg) == "boolean" then + return string.format("(boolean) %s", tostring(arg)) + end +end + +local function fmt_nil(arg) + if type(arg) == "nil" then + return "(nil)" + end +end + +local type_priorities = { + number = 1, + boolean = 2, + string = 3, + table = 4, + ["function"] = 5, + userdata = 6, + thread = 7 +} + +local function is_in_array_part(key, length) + return type(key) == "number" and 1 <= key and key <= length and math.floor(key) == key +end + +local function get_sorted_keys(t) + local keys = {} + local nkeys = 0 + + for key in pairs(t) do + nkeys = nkeys + 1 + keys[nkeys] = key + end + + local length = #t + + local function key_comparator(key1, key2) + local type1, type2 = type(key1), type(key2) + local priority1 = is_in_array_part(key1, length) and 0 or type_priorities[type1] or 8 + local priority2 = is_in_array_part(key2, length) and 0 or type_priorities[type2] or 8 + + if priority1 == priority2 then + if type1 == "string" or type1 == "number" then + return key1 < key2 + elseif type1 == "boolean" then + return key1 -- put true before false + end + else + return priority1 < priority2 + end + end + + table.sort(keys, key_comparator) + return keys, nkeys +end + +local function fmt_table(arg, fmtargs) + if type(arg) ~= "table" then + return + end + + local tmax = assert:get_parameter("TableFormatLevel") + local showrec = assert:get_parameter("TableFormatShowRecursion") + local errchar = assert:get_parameter("TableErrorHighlightCharacter") or "" + local errcolor = assert:get_parameter("TableErrorHighlightColor") + local crumbs = fmtargs and fmtargs.crumbs or {} + local cache = {} + local type_desc + + if getmetatable(arg) == nil then + type_desc = "(" .. tostring(arg) .. ") " + elseif not pcall(setmetatable, arg, getmetatable(arg)) then + -- cannot set same metatable, so it is protected, skip id + type_desc = "(table) " + else + -- unprotected metatable, temporary remove the mt + local mt = getmetatable(arg) + setmetatable(arg, nil) + type_desc = "(" .. tostring(arg) .. ") " + setmetatable(arg, mt) + end + + local function ft(t, l, with_crumbs) + if showrec and cache[t] and cache[t] > 0 then + return "{ ... recursive }" + end + + if next(t) == nil then + return "{ }" + end + + if l > tmax and tmax >= 0 then + return "{ ... more }" + end + + local result = "{" + local keys, nkeys = get_sorted_keys(t) + + cache[t] = (cache[t] or 0) + 1 + local crumb = crumbs[#crumbs - l + 1] + + for i = 1, nkeys do + local k = keys[i] + local v = t[k] + local use_crumbs = with_crumbs and k == crumb + + if type(v) == "table" then + v = ft(v, l + 1, use_crumbs) + elseif type(v) == "string" then + v = "'"..v.."'" + end + + local ch = use_crumbs and errchar or "" + local indent = string.rep(" ",l * 2 - ch:len()) + local mark = (ch:len() == 0 and "" or colors[errcolor](ch)) + result = result .. string.format("\n%s%s[%s] = %s", indent, mark, tostr(k), tostr(v)) + end + + cache[t] = cache[t] - 1 + + return result .. " }" + end + + return type_desc .. ft(arg, 1, true) +end + +local function fmt_function(arg) + if type(arg) == "function" then + local debug_info = debug.getinfo(arg) + return string.format("%s @ line %s in %s", tostring(arg), tostring(debug_info.linedefined), tostring(debug_info.source)) + end +end + +local function fmt_userdata(arg) + if type(arg) == "userdata" then + return string.format("(userdata) '%s'", tostring(arg)) + end +end + +local function fmt_thread(arg) + if type(arg) == "thread" then + return string.format("(thread) '%s'", tostring(arg)) + end +end + +local function fmt_matcher(arg) + if not match.is_matcher(arg) then + return + end + local not_inverted = { + [true] = "is.", + [false] = "no.", + } + local args = {} + for idx = 1, arg.arguments.n do + table.insert(args, assert:format({ arg.arguments[idx], n = 1, })[1]) + end + return string.format("(matcher) %s%s(%s)", + not_inverted[arg.mod], + tostring(arg.name), + table.concat(args, ", ")) +end + +local function fmt_arglist(arglist) + if not util.is_arglist(arglist) then + return + end + local formatted_vals = {} + for idx = 1, arglist.n do + table.insert(formatted_vals, assert:format({ arglist[idx], n = 1, })[1]) + end + return "(values list) (" .. table.concat(formatted_vals, ", ") .. ")" +end + +assert:add_formatter(fmt_string) +assert:add_formatter(fmt_number) +assert:add_formatter(fmt_boolean) +assert:add_formatter(fmt_nil) +assert:add_formatter(fmt_table) +assert:add_formatter(fmt_function) +assert:add_formatter(fmt_userdata) +assert:add_formatter(fmt_thread) +assert:add_formatter(fmt_matcher) +assert:add_formatter(fmt_arglist) +-- Set default table display depth for table formatter +assert:set_parameter("TableFormatLevel", 3) +assert:set_parameter("TableFormatShowRecursion", false) +assert:set_parameter("TableErrorHighlightCharacter", "*") +assert:set_parameter("TableErrorHighlightColor", isatty and "red" or "none") diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/init.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/init.lua new file mode 100644 index 0000000..4ecf8b4 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/init.lua @@ -0,0 +1,17 @@ +local assert = require('luassert.assert') + +assert._COPYRIGHT = "Copyright (c) 2018 Olivine Labs, LLC." +assert._DESCRIPTION = "Extends Lua's built-in assertions to provide additional tests and the ability to create your own." +assert._VERSION = "Luassert 1.8.0" + +-- load basic asserts +require('luassert.assertions') +require('luassert.modifiers') +require('luassert.array') +require('luassert.matchers') +require('luassert.formatters') + +-- load default language +require('luassert.languages.en') + +return assert diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/languages/en.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/languages/en.lua new file mode 100644 index 0000000..0d84b6d --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/languages/en.lua @@ -0,0 +1,48 @@ +local s = require('say') + +s:set_namespace('en') + +s:set("assertion.same.positive", "Expected objects to be the same.\nPassed in:\n%s\nExpected:\n%s") +s:set("assertion.same.negative", "Expected objects to not be the same.\nPassed in:\n%s\nDid not expect:\n%s") + +s:set("assertion.equals.positive", "Expected objects to be equal.\nPassed in:\n%s\nExpected:\n%s") +s:set("assertion.equals.negative", "Expected objects to not be equal.\nPassed in:\n%s\nDid not expect:\n%s") + +s:set("assertion.near.positive", "Expected values to be near.\nPassed in:\n%s\nExpected:\n%s +/- %s") +s:set("assertion.near.negative", "Expected values to not be near.\nPassed in:\n%s\nDid not expect:\n%s +/- %s") + +s:set("assertion.matches.positive", "Expected strings to match.\nPassed in:\n%s\nExpected:\n%s") +s:set("assertion.matches.negative", "Expected strings not to match.\nPassed in:\n%s\nDid not expect:\n%s") + +s:set("assertion.unique.positive", "Expected object to be unique:\n%s") +s:set("assertion.unique.negative", "Expected object to not be unique:\n%s") + +s:set("assertion.error.positive", "Expected a different error.\nCaught:\n%s\nExpected:\n%s") +s:set("assertion.error.negative", "Expected no error, but caught:\n%s") + +s:set("assertion.truthy.positive", "Expected to be truthy, but value was:\n%s") +s:set("assertion.truthy.negative", "Expected to not be truthy, but value was:\n%s") + +s:set("assertion.falsy.positive", "Expected to be falsy, but value was:\n%s") +s:set("assertion.falsy.negative", "Expected to not be falsy, but value was:\n%s") + +s:set("assertion.called.positive", "Expected to be called %s time(s), but was called %s time(s)") +s:set("assertion.called.negative", "Expected not to be called exactly %s time(s), but it was.") + +s:set("assertion.called_at_least.positive", "Expected to be called at least %s time(s), but was called %s time(s)") +s:set("assertion.called_at_most.positive", "Expected to be called at most %s time(s), but was called %s time(s)") +s:set("assertion.called_more_than.positive", "Expected to be called more than %s time(s), but was called %s time(s)") +s:set("assertion.called_less_than.positive", "Expected to be called less than %s time(s), but was called %s time(s)") + +s:set("assertion.called_with.positive", "Function was never called with matching arguments.\nCalled with (last call if any):\n%s\nExpected:\n%s") +s:set("assertion.called_with.negative", "Function was called with matching arguments at least once.\nCalled with (last matching call):\n%s\nDid not expect:\n%s") + +s:set("assertion.returned_with.positive", "Function never returned matching arguments.\nReturned (last call if any):\n%s\nExpected:\n%s") +s:set("assertion.returned_with.negative", "Function returned matching arguments at least once.\nReturned (last matching call):\n%s\nDid not expect:\n%s") + +s:set("assertion.returned_arguments.positive", "Expected to be called with %s argument(s), but was called with %s") +s:set("assertion.returned_arguments.negative", "Expected not to be called with %s argument(s), but was called with %s") + +-- errors +s:set("assertion.internal.argtolittle", "the '%s' function requires a minimum of %s arguments, got: %s") +s:set("assertion.internal.badargtype", "bad argument #%s to '%s' (%s expected, got %s)") diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/match.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/match.lua new file mode 100644 index 0000000..671c82f --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/match.lua @@ -0,0 +1,79 @@ +local namespace = require 'luassert.namespaces' +local util = require 'luassert.util' + +local matcher_mt = { + __call = function(self, value) + return self.callback(value) == self.mod + end, +} + +local state_mt = { + __call = function(self, ...) + local keys = util.extract_keys("matcher", self.tokens) + self.tokens = {} + + local matcher + + for _, key in ipairs(keys) do + matcher = namespace.matcher[key] or matcher + end + + if matcher then + for _, key in ipairs(keys) do + if namespace.modifier[key] then + namespace.modifier[key].callback(self) + end + end + + local arguments = util.make_arglist(...) + local matches = matcher.callback(self, arguments, util.errorlevel()) + return setmetatable({ + name = matcher.name, + mod = self.mod, + callback = matches, + arguments = arguments, + }, matcher_mt) + else + local arguments = util.make_arglist(...) + + for _, key in ipairs(keys) do + if namespace.modifier[key] then + namespace.modifier[key].callback(self, arguments, util.errorlevel()) + end + end + end + + return self + end, + + __index = function(self, key) + for token in key:lower():gmatch('[^_]+') do + table.insert(self.tokens, token) + end + + return self + end +} + +local match = { + _ = setmetatable({mod=true, callback=function() return true end}, matcher_mt), + + state = function() return setmetatable({mod=true, tokens={}}, state_mt) end, + + is_matcher = function(object) + return type(object) == "table" and getmetatable(object) == matcher_mt + end, + + is_ref_matcher = function(object) + local ismatcher = (type(object) == "table" and getmetatable(object) == matcher_mt) + return ismatcher and object.name == "ref" + end, +} + +local mt = { + __index = function(self, key) + return rawget(self, key) or self.state()[key] + end, +} + +return setmetatable(match, mt) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/matchers/composite.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/matchers/composite.lua new file mode 100644 index 0000000..e4775e0 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/matchers/composite.lua @@ -0,0 +1,61 @@ +local assert = require('luassert.assert') +local match = require ('luassert.match') +local s = require('say') + +local function none(state, arguments, level) + local level = (level or 1) + 1 + local argcnt = arguments.n + assert(argcnt > 0, s("assertion.internal.argtolittle", { "none", 1, tostring(argcnt) }), level) + for i = 1, argcnt do + assert(match.is_matcher(arguments[i]), s("assertion.internal.badargtype", { 1, "none", "matcher", type(arguments[i]) }), level) + end + + return function(value) + for _, matcher in ipairs(arguments) do + if matcher(value) then + return false + end + end + return true + end +end + +local function any(state, arguments, level) + local level = (level or 1) + 1 + local argcnt = arguments.n + assert(argcnt > 0, s("assertion.internal.argtolittle", { "any", 1, tostring(argcnt) }), level) + for i = 1, argcnt do + assert(match.is_matcher(arguments[i]), s("assertion.internal.badargtype", { 1, "any", "matcher", type(arguments[i]) }), level) + end + + return function(value) + for _, matcher in ipairs(arguments) do + if matcher(value) then + return true + end + end + return false + end +end + +local function all(state, arguments, level) + local level = (level or 1) + 1 + local argcnt = arguments.n + assert(argcnt > 0, s("assertion.internal.argtolittle", { "all", 1, tostring(argcnt) }), level) + for i = 1, argcnt do + assert(match.is_matcher(arguments[i]), s("assertion.internal.badargtype", { 1, "all", "matcher", type(arguments[i]) }), level) + end + + return function(value) + for _, matcher in ipairs(arguments) do + if not matcher(value) then + return false + end + end + return true + end +end + +assert:register("matcher", "none_of", none) +assert:register("matcher", "any_of", any) +assert:register("matcher", "all_of", all) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/matchers/core.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/matchers/core.lua new file mode 100644 index 0000000..4335baf --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/matchers/core.lua @@ -0,0 +1,173 @@ +-- module will return the list of matchers, and registers matchers with the main assert engine + +-- matchers take 1 parameters; +-- 1) state +-- 2) arguments list. The list has a member 'n' with the argument count to check for trailing nils +-- 3) level The level of the error position relative to the called function +-- returns; function (or callable object); a function that, given an argument, returns a boolean + +local assert = require('luassert.assert') +local astate = require('luassert.state') +local util = require('luassert.util') +local s = require('say') + +local function format(val) + return astate.format_argument(val) or tostring(val) +end + +local function unique(state, arguments, level) + local deep = arguments[1] + return function(value) + local list = value + for k,v in pairs(list) do + for k2, v2 in pairs(list) do + if k ~= k2 then + if deep and util.deepcompare(v, v2, true) then + return false + else + if v == v2 then + return false + end + end + end + end + end + return true + end +end + +local function near(state, arguments, level) + local level = (level or 1) + 1 + local argcnt = arguments.n + assert(argcnt > 1, s("assertion.internal.argtolittle", { "near", 2, tostring(argcnt) }), level) + local expected = tonumber(arguments[1]) + local tolerance = tonumber(arguments[2]) + local numbertype = "number or object convertible to a number" + assert(expected, s("assertion.internal.badargtype", { 1, "near", numbertype, format(arguments[1]) }), level) + assert(tolerance, s("assertion.internal.badargtype", { 2, "near", numbertype, format(arguments[2]) }), level) + + return function(value) + local actual = tonumber(value) + if not actual then return false end + return (actual >= expected - tolerance and actual <= expected + tolerance) + end +end + +local function matches(state, arguments, level) + local level = (level or 1) + 1 + local argcnt = arguments.n + assert(argcnt > 0, s("assertion.internal.argtolittle", { "matches", 1, tostring(argcnt) }), level) + local pattern = arguments[1] + local init = arguments[2] + local plain = arguments[3] + assert(type(pattern) == "string", s("assertion.internal.badargtype", { 1, "matches", "string", type(arguments[1]) }), level) + assert(init == nil or tonumber(init), s("assertion.internal.badargtype", { 2, "matches", "number", type(arguments[2]) }), level) + + return function(value) + local actualtype = type(value) + local actual = nil + if actualtype == "string" or actualtype == "number" or + actualtype == "table" and (getmetatable(value) or {}).__tostring then + actual = tostring(value) + end + if not actual then return false end + return (actual:find(pattern, init, plain) ~= nil) + end +end + +local function equals(state, arguments, level) + local level = (level or 1) + 1 + local argcnt = arguments.n + assert(argcnt > 0, s("assertion.internal.argtolittle", { "equals", 1, tostring(argcnt) }), level) + return function(value) + return value == arguments[1] + end +end + +local function same(state, arguments, level) + local level = (level or 1) + 1 + local argcnt = arguments.n + assert(argcnt > 0, s("assertion.internal.argtolittle", { "same", 1, tostring(argcnt) }), level) + return function(value) + if type(value) == 'table' and type(arguments[1]) == 'table' then + local result = util.deepcompare(value, arguments[1], true) + return result + end + return value == arguments[1] + end +end + +local function ref(state, arguments, level) + local level = (level or 1) + 1 + local argcnt = arguments.n + local argtype = type(arguments[1]) + local isobject = (argtype == "table" or argtype == "function" or argtype == "thread" or argtype == "userdata") + assert(argcnt > 0, s("assertion.internal.argtolittle", { "ref", 1, tostring(argcnt) }), level) + assert(isobject, s("assertion.internal.badargtype", { 1, "ref", "object", argtype }), level) + return function(value) + return value == arguments[1] + end +end + +local function is_true(state, arguments, level) + return function(value) + return value == true + end +end + +local function is_false(state, arguments, level) + return function(value) + return value == false + end +end + +local function truthy(state, arguments, level) + return function(value) + return value ~= false and value ~= nil + end +end + +local function falsy(state, arguments, level) + local is_truthy = truthy(state, arguments, level) + return function(value) + return not is_truthy(value) + end +end + +local function is_type(state, arguments, level, etype) + return function(value) + return type(value) == etype + end +end + +local function is_nil(state, arguments, level) return is_type(state, arguments, level, "nil") end +local function is_boolean(state, arguments, level) return is_type(state, arguments, level, "boolean") end +local function is_number(state, arguments, level) return is_type(state, arguments, level, "number") end +local function is_string(state, arguments, level) return is_type(state, arguments, level, "string") end +local function is_table(state, arguments, level) return is_type(state, arguments, level, "table") end +local function is_function(state, arguments, level) return is_type(state, arguments, level, "function") end +local function is_userdata(state, arguments, level) return is_type(state, arguments, level, "userdata") end +local function is_thread(state, arguments, level) return is_type(state, arguments, level, "thread") end + +assert:register("matcher", "true", is_true) +assert:register("matcher", "false", is_false) + +assert:register("matcher", "nil", is_nil) +assert:register("matcher", "boolean", is_boolean) +assert:register("matcher", "number", is_number) +assert:register("matcher", "string", is_string) +assert:register("matcher", "table", is_table) +assert:register("matcher", "function", is_function) +assert:register("matcher", "userdata", is_userdata) +assert:register("matcher", "thread", is_thread) + +assert:register("matcher", "ref", ref) +assert:register("matcher", "same", same) +assert:register("matcher", "matches", matches) +assert:register("matcher", "match", matches) +assert:register("matcher", "near", near) +assert:register("matcher", "equals", equals) +assert:register("matcher", "equal", equals) +assert:register("matcher", "unique", unique) +assert:register("matcher", "truthy", truthy) +assert:register("matcher", "falsy", falsy) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/matchers/init.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/matchers/init.lua new file mode 100644 index 0000000..c0ad62b --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/matchers/init.lua @@ -0,0 +1,3 @@ +-- load basic machers +require('luassert.matchers.core') +require('luassert.matchers.composite') diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/mock.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/mock.lua new file mode 100644 index 0000000..0a3bf3d --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/mock.lua @@ -0,0 +1,61 @@ +-- module will return a mock module table, and will not register any assertions +local spy = require 'luassert.spy' +local stub = require 'luassert.stub' + +local function mock_apply(object, action) + if type(object) ~= "table" then return end + if spy.is_spy(object) then + return object[action](object) + end + for k,v in pairs(object) do + mock_apply(v, action) + end + return object +end + +local mock +mock = { + new = function(object, dostub, func, self, key) + local visited = {} + local function do_mock(object, self, key) + local mock_handlers = { + ["table"] = function() + if spy.is_spy(object) or visited[object] then return end + visited[object] = true + for k,v in pairs(object) do + object[k] = do_mock(v, object, k) + end + return object + end, + ["function"] = function() + if dostub then + return stub(self, key, func) + elseif self==nil then + return spy.new(object) + else + return spy.on(self, key) + end + end + } + local handler = mock_handlers[type(object)] + return handler and handler() or object + end + return do_mock(object, self, key) + end, + + clear = function(object) + return mock_apply(object, "clear") + end, + + revert = function(object) + return mock_apply(object, "revert") + end +} + +return setmetatable(mock, { + __call = function(self, ...) + -- mock originally was a function only. Now that it is a module table + -- the __call method is required for backward compatibility + return mock.new(...) + end +}) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/modifiers.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/modifiers.lua new file mode 100644 index 0000000..9493228 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/modifiers.lua @@ -0,0 +1,19 @@ +-- module will not return anything, only register assertions/modifiers with the main assert engine +local assert = require('luassert.assert') + +local function is(state) + return state +end + +local function is_not(state) + state.mod = not state.mod + return state +end + +assert:register("modifier", "is", is) +assert:register("modifier", "are", is) +assert:register("modifier", "was", is) +assert:register("modifier", "has", is) +assert:register("modifier", "does", is) +assert:register("modifier", "not", is_not) +assert:register("modifier", "no", is_not) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/namespaces.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/namespaces.lua new file mode 100644 index 0000000..0790fce --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/namespaces.lua @@ -0,0 +1,2 @@ +-- stores the list of namespaces +return {} diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/spy.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/spy.lua new file mode 100644 index 0000000..eb7fc06 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/spy.lua @@ -0,0 +1,195 @@ +-- module will return spy table, and register its assertions with the main assert engine +local assert = require('luassert.assert') +local util = require('luassert.util') + +-- Spy metatable +local spy_mt = { + __call = function(self, ...) + local arguments = util.make_arglist(...) + table.insert(self.calls, util.copyargs(arguments)) + local function get_returns(...) + local returnvals = util.make_arglist(...) + table.insert(self.returnvals, util.copyargs(returnvals)) + return ... + end + return get_returns(self.callback(...)) + end +} + +local spy -- must make local before defining table, because table contents refers to the table (recursion) +spy = { + new = function(callback) + callback = callback or function() end + if not util.callable(callback) then + error("Cannot spy on type '" .. type(callback) .. "', only on functions or callable elements", util.errorlevel()) + end + local s = setmetatable({ + calls = {}, + returnvals = {}, + callback = callback, + + target_table = nil, -- these will be set when using 'spy.on' + target_key = nil, + + revert = function(self) + if not self.reverted then + if self.target_table and self.target_key then + self.target_table[self.target_key] = self.callback + end + self.reverted = true + end + return self.callback + end, + + clear = function(self) + self.calls = {} + self.returnvals = {} + return self + end, + + called = function(self, times, compare) + if times or compare then + local compare = compare or function(count, expected) return count == expected end + return compare(#self.calls, times), #self.calls + end + + return (#self.calls > 0), #self.calls + end, + + called_with = function(self, args) + local last_arglist = nil + if #self.calls > 0 then + last_arglist = self.calls[#self.calls].vals + end + local matching_arglists = util.matchargs(self.calls, args) + if matching_arglists ~= nil then + return true, matching_arglists.vals + end + return false, last_arglist + end, + + returned_with = function(self, args) + local last_returnvallist = nil + if #self.returnvals > 0 then + last_returnvallist = self.returnvals[#self.returnvals].vals + end + local matching_returnvallists = util.matchargs(self.returnvals, args) + if matching_returnvallists ~= nil then + return true, matching_returnvallists.vals + end + return false, last_returnvallist + end + }, spy_mt) + assert:add_spy(s) -- register with the current state + return s + end, + + is_spy = function(object) + return type(object) == "table" and getmetatable(object) == spy_mt + end, + + on = function(target_table, target_key) + local s = spy.new(target_table[target_key]) + target_table[target_key] = s + -- store original data + s.target_table = target_table + s.target_key = target_key + + return s + end +} + +local function set_spy(state, arguments, level) + state.payload = arguments[1] + if arguments[2] ~= nil then + state.failure_message = arguments[2] + end +end + +local function returned_with(state, arguments, level) + local level = (level or 1) + 1 + local payload = rawget(state, "payload") + if payload and payload.returned_with then + local assertion_holds, matching_or_last_returnvallist = state.payload:returned_with(arguments) + local expected_returnvallist = util.shallowcopy(arguments) + util.cleararglist(arguments) + util.tinsert(arguments, 1, matching_or_last_returnvallist) + util.tinsert(arguments, 2, expected_returnvallist) + return assertion_holds + else + error("'returned_with' must be chained after 'spy(aspy)'", level) + end +end + +local function called_with(state, arguments, level) + local level = (level or 1) + 1 + local payload = rawget(state, "payload") + if payload and payload.called_with then + local assertion_holds, matching_or_last_arglist = state.payload:called_with(arguments) + local expected_arglist = util.shallowcopy(arguments) + util.cleararglist(arguments) + util.tinsert(arguments, 1, matching_or_last_arglist) + util.tinsert(arguments, 2, expected_arglist) + return assertion_holds + else + error("'called_with' must be chained after 'spy(aspy)'", level) + end +end + +local function called(state, arguments, level, compare) + local level = (level or 1) + 1 + local num_times = arguments[1] + if not num_times and not state.mod then + state.mod = true + num_times = 0 + end + local payload = rawget(state, "payload") + if payload and type(payload) == "table" and payload.called then + local result, count = state.payload:called(num_times, compare) + arguments[1] = tostring(num_times or ">0") + util.tinsert(arguments, 2, tostring(count)) + arguments.nofmt = arguments.nofmt or {} + arguments.nofmt[1] = true + arguments.nofmt[2] = true + return result + elseif payload and type(payload) == "function" then + error("When calling 'spy(aspy)', 'aspy' must not be the original function, but the spy function replacing the original", level) + else + error("'called' must be chained after 'spy(aspy)'", level) + end +end + +local function called_at_least(state, arguments, level) + local level = (level or 1) + 1 + return called(state, arguments, level, function(count, expected) return count >= expected end) +end + +local function called_at_most(state, arguments, level) + local level = (level or 1) + 1 + return called(state, arguments, level, function(count, expected) return count <= expected end) +end + +local function called_more_than(state, arguments, level) + local level = (level or 1) + 1 + return called(state, arguments, level, function(count, expected) return count > expected end) +end + +local function called_less_than(state, arguments, level) + local level = (level or 1) + 1 + return called(state, arguments, level, function(count, expected) return count < expected end) +end + +assert:register("modifier", "spy", set_spy) +assert:register("assertion", "returned_with", returned_with, "assertion.returned_with.positive", "assertion.returned_with.negative") +assert:register("assertion", "called_with", called_with, "assertion.called_with.positive", "assertion.called_with.negative") +assert:register("assertion", "called", called, "assertion.called.positive", "assertion.called.negative") +assert:register("assertion", "called_at_least", called_at_least, "assertion.called_at_least.positive", "assertion.called_less_than.positive") +assert:register("assertion", "called_at_most", called_at_most, "assertion.called_at_most.positive", "assertion.called_more_than.positive") +assert:register("assertion", "called_more_than", called_more_than, "assertion.called_more_than.positive", "assertion.called_at_most.positive") +assert:register("assertion", "called_less_than", called_less_than, "assertion.called_less_than.positive", "assertion.called_at_least.positive") + +return setmetatable(spy, { + __call = function(self, ...) + return spy.new(...) + end +}) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/state.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/state.lua new file mode 100644 index 0000000..6de0efe --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/state.lua @@ -0,0 +1,127 @@ +-- maintains a state of the assert engine in a linked-list fashion +-- records; formatters, parameters, spies and stubs + +local state_mt = { + __call = function(self) + self:revert() + end +} + +local spies_mt = { __mode = "kv" } + +local nilvalue = {} -- unique ID to refer to nil values for parameters + +-- will hold the current state +local current + +-- exported module table +local state = {} + +------------------------------------------------------ +-- Reverts to a (specific) snapshot. +-- @param self (optional) the snapshot to revert to. If not provided, it will revert to the last snapshot. +state.revert = function(self) + if not self then + -- no snapshot given, so move 1 up + self = current + if not self.previous then + -- top of list, no previous one, nothing to do + return + end + end + if getmetatable(self) ~= state_mt then error("Value provided is not a valid snapshot", 2) end + + if self.next then + self.next:revert() + end + -- revert formatters in 'last' + self.formatters = {} + -- revert parameters in 'last' + self.parameters = {} + -- revert spies/stubs in 'last' + for s,_ in pairs(self.spies) do + self.spies[s] = nil + s:revert() + end + setmetatable(self, nil) -- invalidate as a snapshot + current = self.previous + current.next = nil +end + +------------------------------------------------------ +-- Creates a new snapshot. +-- @return snapshot table +state.snapshot = function() + local new = setmetatable ({ + formatters = {}, + parameters = {}, + spies = setmetatable({}, spies_mt), + previous = current, + revert = state.revert, + }, state_mt) + if current then current.next = new end + current = new + return current +end + + +-- FORMATTERS +state.add_formatter = function(callback) + table.insert(current.formatters, 1, callback) +end + +state.remove_formatter = function(callback, s) + s = s or current + for i, v in ipairs(s.formatters) do + if v == callback then + table.remove(s.formatters, i) + break + end + end + -- wasn't found, so traverse up 1 state + if s.previous then + state.remove_formatter(callback, s.previous) + end +end + +state.format_argument = function(val, s, fmtargs) + s = s or current + for _, fmt in ipairs(s.formatters) do + local valfmt = fmt(val, fmtargs) + if valfmt ~= nil then return valfmt end + end + -- nothing found, check snapshot 1 up in list + if s.previous then + return state.format_argument(val, s.previous, fmtargs) + end + return nil -- end of list, couldn't format +end + + +-- PARAMETERS +state.set_parameter = function(name, value) + if value == nil then value = nilvalue end + current.parameters[name] = value +end + +state.get_parameter = function(name, s) + s = s or current + local val = s.parameters[name] + if val == nil and s.previous then + -- not found, so check 1 up in list + return state.get_parameter(name, s.previous) + end + if val ~= nilvalue then + return val + end + return nil +end + +-- SPIES / STUBS +state.add_spy = function(spy) + current.spies[spy] = true +end + +state.snapshot() -- create initial state + +return state diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/stub.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/stub.lua new file mode 100644 index 0000000..91ae6e0 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/stub.lua @@ -0,0 +1,107 @@ +-- module will return a stub module table +local assert = require 'luassert.assert' +local spy = require 'luassert.spy' +local util = require 'luassert.util' +local unpack = util.unpack +local pack = util.pack + +local stub = {} + +function stub.new(object, key, ...) + if object == nil and key == nil then + -- called without arguments, create a 'blank' stub + object = {} + key = "" + end + local return_values = pack(...) + assert(type(object) == "table" and key ~= nil, "stub.new(): Can only create stub on a table key, call with 2 params; table, key", util.errorlevel()) + assert(object[key] == nil or util.callable(object[key]), "stub.new(): The element for which to create a stub must either be callable, or be nil", util.errorlevel()) + local old_elem = object[key] -- keep existing element (might be nil!) + + local fn = (return_values.n == 1 and util.callable(return_values[1]) and return_values[1]) + local defaultfunc = fn or function() + return unpack(return_values) + end + local oncalls = {} + local callbacks = {} + local stubfunc = function(...) + local args = util.make_arglist(...) + local match = util.matchoncalls(oncalls, args) + if match then + return callbacks[match](...) + end + return defaultfunc(...) + end + + object[key] = stubfunc -- set the stubfunction + local s = spy.on(object, key) -- create a spy on top of the stub function + local spy_revert = s.revert -- keep created revert function + + s.revert = function(self) -- wrap revert function to restore original element + if not self.reverted then + spy_revert(self) + object[key] = old_elem + self.reverted = true + end + return old_elem + end + + s.returns = function(...) + local return_args = pack(...) + defaultfunc = function() + return unpack(return_args) + end + return s + end + + s.invokes = function(func) + defaultfunc = function(...) + return func(...) + end + return s + end + + s.by_default = { + returns = s.returns, + invokes = s.invokes, + } + + s.on_call_with = function(...) + local match_args = util.make_arglist(...) + match_args = util.copyargs(match_args) + return { + returns = function(...) + local return_args = pack(...) + table.insert(oncalls, match_args) + callbacks[match_args] = function() + return unpack(return_args) + end + return s + end, + invokes = function(func) + table.insert(oncalls, match_args) + callbacks[match_args] = function(...) + return func(...) + end + return s + end + } + end + + return s +end + +local function set_stub(state, arguments) + state.payload = arguments[1] + state.failure_message = arguments[2] +end + +assert:register("modifier", "stub", set_stub) + +return setmetatable(stub, { + __call = function(self, ...) + -- stub originally was a function only. Now that it is a module table + -- the __call method is required for backward compatibility + return stub.new(...) + end +}) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/util.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/util.lua new file mode 100644 index 0000000..da2f247 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/luassert/util.lua @@ -0,0 +1,362 @@ +local util = {} +local arglist_mt = {} + +-- have pack/unpack both respect the 'n' field +local _unpack = table.unpack or unpack +local unpack = function(t, i, j) return _unpack(t, i or 1, j or t.n or #t) end +local pack = function(...) return { n = select("#", ...), ... } end +util.pack = pack +util.unpack = unpack + + +function util.deepcompare(t1,t2,ignore_mt,cycles,thresh1,thresh2) + local ty1 = type(t1) + local ty2 = type(t2) + -- non-table types can be directly compared + if ty1 ~= 'table' or ty2 ~= 'table' then return t1 == t2 end + local mt1 = debug.getmetatable(t1) + local mt2 = debug.getmetatable(t2) + -- would equality be determined by metatable __eq? + if mt1 and mt1 == mt2 and mt1.__eq then + -- then use that unless asked not to + if not ignore_mt then return t1 == t2 end + else -- we can skip the deep comparison below if t1 and t2 share identity + if rawequal(t1, t2) then return true end + end + + -- handle recursive tables + cycles = cycles or {{},{}} + thresh1, thresh2 = (thresh1 or 1), (thresh2 or 1) + cycles[1][t1] = (cycles[1][t1] or 0) + cycles[2][t2] = (cycles[2][t2] or 0) + if cycles[1][t1] == 1 or cycles[2][t2] == 1 then + thresh1 = cycles[1][t1] + 1 + thresh2 = cycles[2][t2] + 1 + end + if cycles[1][t1] > thresh1 and cycles[2][t2] > thresh2 then + return true + end + + cycles[1][t1] = cycles[1][t1] + 1 + cycles[2][t2] = cycles[2][t2] + 1 + + for k1,v1 in next, t1 do + local v2 = t2[k1] + if v2 == nil then + return false, {k1} + end + + local same, crumbs = util.deepcompare(v1,v2,nil,cycles,thresh1,thresh2) + if not same then + crumbs = crumbs or {} + table.insert(crumbs, k1) + return false, crumbs + end + end + for k2,_ in next, t2 do + -- only check whether each element has a t1 counterpart, actual comparison + -- has been done in first loop above + if t1[k2] == nil then return false, {k2} end + end + + cycles[1][t1] = cycles[1][t1] - 1 + cycles[2][t2] = cycles[2][t2] - 1 + + return true +end + +function util.shallowcopy(t) + if type(t) ~= "table" then return t end + local copy = {} + setmetatable(copy, getmetatable(t)) + for k,v in next, t do + copy[k] = v + end + return copy +end + +function util.deepcopy(t, deepmt, cache) + local spy = require 'luassert.spy' + if type(t) ~= "table" then return t end + local copy = {} + + -- handle recursive tables + local cache = cache or {} + if cache[t] then return cache[t] end + cache[t] = copy + + for k,v in next, t do + copy[k] = (spy.is_spy(v) and v or util.deepcopy(v, deepmt, cache)) + end + if deepmt then + debug.setmetatable(copy, util.deepcopy(debug.getmetatable(t), false, cache)) + else + debug.setmetatable(copy, debug.getmetatable(t)) + end + return copy +end + +----------------------------------------------- +-- Copies arguments as a list of arguments +-- @param args the arguments of which to copy +-- @return the copy of the arguments +function util.copyargs(args) + local copy = {} + setmetatable(copy, getmetatable(args)) + local match = require 'luassert.match' + local spy = require 'luassert.spy' + for k,v in pairs(args) do + copy[k] = ((match.is_matcher(v) or spy.is_spy(v)) and v or util.deepcopy(v)) + end + return { vals = copy, refs = util.shallowcopy(args) } +end + +----------------------------------------------- +-- Clear an arguments or return values list from a table +-- @param arglist the table to clear of arguments or return values and their count +-- @return No return values +function util.cleararglist(arglist) + for idx = arglist.n, 1, -1 do + util.tremove(arglist, idx) + end + arglist.n = nil +end + +----------------------------------------------- +-- Test specs against an arglist in deepcopy and refs flavours. +-- @param args deepcopy arglist +-- @param argsrefs refs arglist +-- @param specs arguments/return values to match against args/argsrefs +-- @return true if specs match args/argsrefs, false otherwise +local function matcharg(args, argrefs, specs) + local match = require 'luassert.match' + for idx, argval in pairs(args) do + local spec = specs[idx] + if match.is_matcher(spec) then + if match.is_ref_matcher(spec) then + argval = argrefs[idx] + end + if not spec(argval) then + return false + end + elseif (spec == nil or not util.deepcompare(argval, spec)) then + return false + end + end + + for idx, spec in pairs(specs) do + -- only check whether each element has an args counterpart, + -- actual comparison has been done in first loop above + local argval = args[idx] + if argval == nil then + -- no args counterpart, so try to compare using matcher + if match.is_matcher(spec) then + if not spec(argval) then + return false + end + else + return false + end + end + end + return true +end + +----------------------------------------------- +-- Find matching arguments/return values in a saved list of +-- arguments/returned values. +-- @param invocations_list list of arguments/returned values to search (list of lists) +-- @param specs arguments/return values to match against argslist +-- @return the last matching arguments/returned values if a match is found, otherwise nil +function util.matchargs(invocations_list, specs) + -- Search the arguments/returned values last to first to give the + -- most helpful answer possible. In the cases where you can place + -- your assertions between calls to check this gives you the best + -- information if no calls match. In the cases where you can't do + -- that there is no good way to predict what would work best. + assert(not util.is_arglist(invocations_list), "expected a list of arglist-object, got an arglist") + for ii = #invocations_list, 1, -1 do + local val = invocations_list[ii] + if matcharg(val.vals, val.refs, specs) then + return val + end + end + return nil +end + +----------------------------------------------- +-- Find matching oncall for an actual call. +-- @param oncalls list of oncalls to search +-- @param args actual call argslist to match against +-- @return the first matching oncall if a match is found, otherwise nil +function util.matchoncalls(oncalls, args) + for _, callspecs in ipairs(oncalls) do + -- This lookup is done immediately on *args* passing into the stub + -- so pass *args* as both *args* and *argsref* without copying + -- either. + if matcharg(args, args, callspecs.vals) then + return callspecs + end + end + return nil +end + +----------------------------------------------- +-- table.insert() replacement that respects nil values. +-- The function will use table field 'n' as indicator of the +-- table length, if not set, it will be added. +-- @param t table into which to insert +-- @param pos (optional) position in table where to insert. NOTE: not optional if you want to insert a nil-value! +-- @param val value to insert +-- @return No return values +function util.tinsert(...) + -- check optional POS value + local args = {...} + local c = select('#',...) + local t = args[1] + local pos = args[2] + local val = args[3] + if c < 3 then + val = pos + pos = nil + end + -- set length indicator n if not present (+1) + t.n = (t.n or #t) + 1 + if not pos then + pos = t.n + elseif pos > t.n then + -- out of our range + t[pos] = val + t.n = pos + end + -- shift everything up 1 pos + for i = t.n, pos + 1, -1 do + t[i]=t[i-1] + end + -- add element to be inserted + t[pos] = val +end +----------------------------------------------- +-- table.remove() replacement that respects nil values. +-- The function will use table field 'n' as indicator of the +-- table length, if not set, it will be added. +-- @param t table from which to remove +-- @param pos (optional) position in table to remove +-- @return No return values +function util.tremove(t, pos) + -- set length indicator n if not present (+1) + t.n = t.n or #t + if not pos then + pos = t.n + elseif pos > t.n then + local removed = t[pos] + -- out of our range + t[pos] = nil + return removed + end + local removed = t[pos] + -- shift everything up 1 pos + for i = pos, t.n do + t[i]=t[i+1] + end + -- set size, clean last + t[t.n] = nil + t.n = t.n - 1 + return removed +end + +----------------------------------------------- +-- Checks an element to be callable. +-- The type must either be a function or have a metatable +-- containing an '__call' function. +-- @param object element to inspect on being callable or not +-- @return boolean, true if the object is callable +function util.callable(object) + return type(object) == "function" or type((debug.getmetatable(object) or {}).__call) == "function" +end + +----------------------------------------------- +-- Checks an element has tostring. +-- The type must either be a string or have a metatable +-- containing an '__tostring' function. +-- @param object element to inspect on having tostring or not +-- @return boolean, true if the object has tostring +function util.hastostring(object) + return type(object) == "string" or type((debug.getmetatable(object) or {}).__tostring) == "function" +end + +----------------------------------------------- +-- Find the first level, not defined in the same file as the caller's +-- code file to properly report an error. +-- @param level the level to use as the caller's source file +-- @return number, the level of which to report an error +function util.errorlevel(level) + local level = (level or 1) + 1 -- add one to get level of the caller + local info = debug.getinfo(level) + local source = (info or {}).source + local file = source + while file and (file == source or source == "=(tail call)") do + level = level + 1 + info = debug.getinfo(level) + source = (info or {}).source + end + if level > 1 then level = level - 1 end -- deduct call to errorlevel() itself + return level +end + +----------------------------------------------- +-- Extract modifier and namespace keys from list of tokens. +-- @param nspace the namespace from which to match tokens +-- @param tokens list of tokens to search for keys +-- @return table, list of keys that were extracted +function util.extract_keys(nspace, tokens) + local namespace = require 'luassert.namespaces' + + -- find valid keys by coalescing tokens as needed, starting from the end + local keys = {} + local key = nil + local i = #tokens + while i > 0 do + local token = tokens[i] + key = key and (token .. '_' .. key) or token + + -- find longest matching key in the given namespace + local longkey = i > 1 and (tokens[i-1] .. '_' .. key) or nil + while i > 1 and longkey and namespace[nspace][longkey] do + key = longkey + i = i - 1 + token = tokens[i] + longkey = (token .. '_' .. key) + end + + if namespace.modifier[key] or namespace[nspace][key] then + table.insert(keys, 1, key) + key = nil + end + i = i - 1 + end + + -- if there's anything left we didn't recognize it + if key then + error("luassert: unknown modifier/" .. nspace .. ": '" .. key .."'", util.errorlevel(2)) + end + + return keys +end + +----------------------------------------------- +-- store argument list for return values of a function in a table. +-- The table will get a metatable to identify it as an arglist +function util.make_arglist(...) + local arglist = { ... } + arglist.n = select('#', ...) -- add values count for trailing nils + return setmetatable(arglist, arglist_mt) +end + +----------------------------------------------- +-- check a table to be an arglist type. +function util.is_arglist(object) + return getmetatable(object) == arglist_mt +end + +return util diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/_meta/_luassert.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/_meta/_luassert.lua new file mode 100644 index 0000000..c589970 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/_meta/_luassert.lua @@ -0,0 +1,289 @@ +---@meta +---This file is autogenerated, DO NOT EDIT +error "Cannot require a meta file" + +---@generic T:any +---@alias LuassertFunction fun(value:T, message?:string):T +---@alias LuassertFunctionTwoArgs fun(expected:T, actual:T, message?:string):T +---@alias LuassertFunctionMultiArgs fun(...:T):T + +---@class Luassert +---@field are_boolean LuassertFunction +---@field are_equal LuassertFunctionTwoArgs +---@field are_equals LuassertFunctionTwoArgs +---@field are_error LuassertFunction +---@field are_error_match LuassertFunctionTwoArgs +---@field are_error_matches LuassertFunctionTwoArgs +---@field are_errors LuassertFunction +---@field are_false LuassertFunction +---@field are_falsy LuassertFunction +---@field are_function LuassertFunction +---@field are_holes LuassertFunction +---@field are_match LuassertFunctionTwoArgs +---@field are_match_error LuassertFunctionTwoArgs +---@field are_matches LuassertFunctionTwoArgs +---@field are_matches_error LuassertFunctionTwoArgs +---@field are_near LuassertFunctionMultiArgs +---@field are_nil LuassertFunction +---@field are_number LuassertFunction +---@field are_returned_arguments LuassertFunction +---@field are_same LuassertFunctionTwoArgs +---@field are_string LuassertFunction +---@field are_table LuassertFunction +---@field are_thread LuassertFunction +---@field are_true LuassertFunction +---@field are_truthy LuassertFunction +---@field are_unique LuassertFunction +---@field are_userdata LuassertFunction +---@field array_boolean LuassertFunction +---@field array_equal LuassertFunctionTwoArgs +---@field array_equals LuassertFunctionTwoArgs +---@field array_error LuassertFunction +---@field array_error_match LuassertFunctionTwoArgs +---@field array_error_matches LuassertFunctionTwoArgs +---@field array_errors LuassertFunction +---@field array_false LuassertFunction +---@field array_falsy LuassertFunction +---@field array_function LuassertFunction +---@field array_holes LuassertFunction +---@field array_match LuassertFunctionTwoArgs +---@field array_match_error LuassertFunctionTwoArgs +---@field array_matches LuassertFunctionTwoArgs +---@field array_matches_error LuassertFunctionTwoArgs +---@field array_near LuassertFunctionMultiArgs +---@field array_nil LuassertFunction +---@field array_number LuassertFunction +---@field array_returned_arguments LuassertFunction +---@field array_same LuassertFunctionTwoArgs +---@field array_string LuassertFunction +---@field array_table LuassertFunction +---@field array_thread LuassertFunction +---@field array_true LuassertFunction +---@field array_truthy LuassertFunction +---@field array_unique LuassertFunction +---@field array_userdata LuassertFunction +---@field does_boolean LuassertFunction +---@field does_equal LuassertFunctionTwoArgs +---@field does_equals LuassertFunctionTwoArgs +---@field does_error LuassertFunction +---@field does_error_match LuassertFunctionTwoArgs +---@field does_error_matches LuassertFunctionTwoArgs +---@field does_errors LuassertFunction +---@field does_false LuassertFunction +---@field does_falsy LuassertFunction +---@field does_function LuassertFunction +---@field does_holes LuassertFunction +---@field does_match LuassertFunctionTwoArgs +---@field does_match_error LuassertFunctionTwoArgs +---@field does_matches LuassertFunctionTwoArgs +---@field does_matches_error LuassertFunctionTwoArgs +---@field does_near LuassertFunctionMultiArgs +---@field does_nil LuassertFunction +---@field does_number LuassertFunction +---@field does_returned_arguments LuassertFunction +---@field does_same LuassertFunctionTwoArgs +---@field does_string LuassertFunction +---@field does_table LuassertFunction +---@field does_thread LuassertFunction +---@field does_true LuassertFunction +---@field does_truthy LuassertFunction +---@field does_unique LuassertFunction +---@field does_userdata LuassertFunction +---@field has_boolean LuassertFunction +---@field has_equal LuassertFunctionTwoArgs +---@field has_equals LuassertFunctionTwoArgs +---@field has_error LuassertFunction +---@field has_error_match LuassertFunctionTwoArgs +---@field has_error_matches LuassertFunctionTwoArgs +---@field has_errors LuassertFunction +---@field has_false LuassertFunction +---@field has_falsy LuassertFunction +---@field has_function LuassertFunction +---@field has_holes LuassertFunction +---@field has_match LuassertFunctionTwoArgs +---@field has_match_error LuassertFunctionTwoArgs +---@field has_matches LuassertFunctionTwoArgs +---@field has_matches_error LuassertFunctionTwoArgs +---@field has_near LuassertFunctionMultiArgs +---@field has_nil LuassertFunction +---@field has_number LuassertFunction +---@field has_returned_arguments LuassertFunction +---@field has_same LuassertFunctionTwoArgs +---@field has_string LuassertFunction +---@field has_table LuassertFunction +---@field has_thread LuassertFunction +---@field has_true LuassertFunction +---@field has_truthy LuassertFunction +---@field has_unique LuassertFunction +---@field has_userdata LuassertFunction +---@field is_boolean LuassertFunction +---@field is_equal LuassertFunctionTwoArgs +---@field is_equals LuassertFunctionTwoArgs +---@field is_error LuassertFunction +---@field is_error_match LuassertFunctionTwoArgs +---@field is_error_matches LuassertFunctionTwoArgs +---@field is_errors LuassertFunction +---@field is_false LuassertFunction +---@field is_falsy LuassertFunction +---@field is_function LuassertFunction +---@field is_holes LuassertFunction +---@field is_match LuassertFunctionTwoArgs +---@field is_match_error LuassertFunctionTwoArgs +---@field is_matches LuassertFunctionTwoArgs +---@field is_matches_error LuassertFunctionTwoArgs +---@field is_near LuassertFunctionMultiArgs +---@field is_nil LuassertFunction +---@field is_number LuassertFunction +---@field is_returned_arguments LuassertFunction +---@field is_same LuassertFunctionTwoArgs +---@field is_string LuassertFunction +---@field is_table LuassertFunction +---@field is_thread LuassertFunction +---@field is_true LuassertFunction +---@field is_truthy LuassertFunction +---@field is_unique LuassertFunction +---@field is_userdata LuassertFunction +---@field message_boolean LuassertFunction +---@field message_equal LuassertFunctionTwoArgs +---@field message_equals LuassertFunctionTwoArgs +---@field message_error LuassertFunction +---@field message_error_match LuassertFunctionTwoArgs +---@field message_error_matches LuassertFunctionTwoArgs +---@field message_errors LuassertFunction +---@field message_false LuassertFunction +---@field message_falsy LuassertFunction +---@field message_function LuassertFunction +---@field message_holes LuassertFunction +---@field message_match LuassertFunctionTwoArgs +---@field message_match_error LuassertFunctionTwoArgs +---@field message_matches LuassertFunctionTwoArgs +---@field message_matches_error LuassertFunctionTwoArgs +---@field message_near LuassertFunctionMultiArgs +---@field message_nil LuassertFunction +---@field message_number LuassertFunction +---@field message_returned_arguments LuassertFunction +---@field message_same LuassertFunctionTwoArgs +---@field message_string LuassertFunction +---@field message_table LuassertFunction +---@field message_thread LuassertFunction +---@field message_true LuassertFunction +---@field message_truthy LuassertFunction +---@field message_unique LuassertFunction +---@field message_userdata LuassertFunction +---@field no_boolean LuassertFunction +---@field no_equal LuassertFunctionTwoArgs +---@field no_equals LuassertFunctionTwoArgs +---@field no_error LuassertFunction +---@field no_error_match LuassertFunctionTwoArgs +---@field no_error_matches LuassertFunctionTwoArgs +---@field no_errors LuassertFunction +---@field no_false LuassertFunction +---@field no_falsy LuassertFunction +---@field no_function LuassertFunction +---@field no_holes LuassertFunction +---@field no_match LuassertFunctionTwoArgs +---@field no_match_error LuassertFunctionTwoArgs +---@field no_matches LuassertFunctionTwoArgs +---@field no_matches_error LuassertFunctionTwoArgs +---@field no_near LuassertFunctionMultiArgs +---@field no_nil LuassertFunction +---@field no_number LuassertFunction +---@field no_returned_arguments LuassertFunction +---@field no_same LuassertFunctionTwoArgs +---@field no_string LuassertFunction +---@field no_table LuassertFunction +---@field no_thread LuassertFunction +---@field no_true LuassertFunction +---@field no_truthy LuassertFunction +---@field no_unique LuassertFunction +---@field no_userdata LuassertFunction +---@field not_boolean LuassertFunction +---@field not_equal LuassertFunctionTwoArgs +---@field not_equals LuassertFunctionTwoArgs +---@field not_error LuassertFunction +---@field not_error_match LuassertFunctionTwoArgs +---@field not_error_matches LuassertFunctionTwoArgs +---@field not_errors LuassertFunction +---@field not_false LuassertFunction +---@field not_falsy LuassertFunction +---@field not_function LuassertFunction +---@field not_holes LuassertFunction +---@field not_match LuassertFunctionTwoArgs +---@field not_match_error LuassertFunctionTwoArgs +---@field not_matches LuassertFunctionTwoArgs +---@field not_matches_error LuassertFunctionTwoArgs +---@field not_near LuassertFunctionMultiArgs +---@field not_nil LuassertFunction +---@field not_number LuassertFunction +---@field not_returned_arguments LuassertFunction +---@field not_same LuassertFunctionTwoArgs +---@field not_string LuassertFunction +---@field not_table LuassertFunction +---@field not_thread LuassertFunction +---@field not_true LuassertFunction +---@field not_truthy LuassertFunction +---@field not_unique LuassertFunction +---@field not_userdata LuassertFunction +---@field was_boolean LuassertFunction +---@field was_equal LuassertFunctionTwoArgs +---@field was_equals LuassertFunctionTwoArgs +---@field was_error LuassertFunction +---@field was_error_match LuassertFunctionTwoArgs +---@field was_error_matches LuassertFunctionTwoArgs +---@field was_errors LuassertFunction +---@field was_false LuassertFunction +---@field was_falsy LuassertFunction +---@field was_function LuassertFunction +---@field was_holes LuassertFunction +---@field was_match LuassertFunctionTwoArgs +---@field was_match_error LuassertFunctionTwoArgs +---@field was_matches LuassertFunctionTwoArgs +---@field was_matches_error LuassertFunctionTwoArgs +---@field was_near LuassertFunctionMultiArgs +---@field was_nil LuassertFunction +---@field was_number LuassertFunction +---@field was_returned_arguments LuassertFunction +---@field was_same LuassertFunctionTwoArgs +---@field was_string LuassertFunction +---@field was_table LuassertFunction +---@field was_thread LuassertFunction +---@field was_true LuassertFunction +---@field was_truthy LuassertFunction +---@field was_unique LuassertFunction +---@field was_userdata LuassertFunction +---@field boolean LuassertFunction +---@field equal LuassertFunctionTwoArgs +---@field equals LuassertFunctionTwoArgs +---@field error LuassertFunction +---@field error_match LuassertFunctionTwoArgs +---@field error_matches LuassertFunctionTwoArgs +---@field errors LuassertFunction +---@field False LuassertFunction +---@field falsy LuassertFunction +---@field Function LuassertFunction +---@field holes LuassertFunction +---@field match LuassertFunctionTwoArgs +---@field match_error LuassertFunctionTwoArgs +---@field matches LuassertFunctionTwoArgs +---@field matches_error LuassertFunctionTwoArgs +---@field near LuassertFunctionMultiArgs +---@field Nil LuassertFunction +---@field number LuassertFunction +---@field returned_arguments LuassertFunction +---@field same LuassertFunctionTwoArgs +---@field string LuassertFunction +---@field table LuassertFunction +---@field thread LuassertFunction +---@field True LuassertFunction +---@field truthy LuassertFunction +---@field unique LuassertFunction +---@field userdata LuassertFunction +---@field are Luassert +---@field array Luassert +---@field does Luassert +---@field has Luassert +---@field is Luassert +---@field message Luassert +---@field no Luassert +---@field Not Luassert +---@field was Luassert \ No newline at end of file diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/api.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/api.lua new file mode 100644 index 0000000..2a4e9c0 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/api.lua @@ -0,0 +1,14 @@ +local util = require "plenary.async.util" + +return setmetatable({}, { + __index = function(t, k) + return function(...) + -- if we are in a fast event await the scheduler + if vim.in_fast_event() then + util.scheduler() + end + + return vim.api[k](...) + end + end, +}) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/async.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/async.lua new file mode 100644 index 0000000..ff49288 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/async.lua @@ -0,0 +1,122 @@ +local co = coroutine +local vararg = require "plenary.vararg" +local errors = require "plenary.errors" +local traceback_error = errors.traceback_error +local f = require "plenary.functional" + +local M = {} + +local function is_callable(fn) + return type(fn) == "function" or (type(fn) == "table" and type(getmetatable(fn)["__call"]) == "function") +end + +---because we can't store varargs +local function callback_or_next(step, thread, callback, ...) + local stat = f.first(...) + + if not stat then + error(string.format("The coroutine failed with this message: %s", f.second(...))) + end + + if co.status(thread) == "dead" then + if callback == nil then + return + end + callback(select(2, ...)) + else + local returned_function = f.second(...) + local nargs = f.third(...) + + assert(is_callable(returned_function), "type error :: expected func") + returned_function(vararg.rotate(nargs, step, select(4, ...))) + end +end + +---Executes a future with a callback when it is done +---@param async_function Future: the future to execute +---@param callback function: the callback to call when done +local execute = function(async_function, callback, ...) + assert(is_callable(async_function), "type error :: expected func") + + local thread = co.create(async_function) + + local step + step = function(...) + callback_or_next(step, thread, callback, co.resume(thread, ...)) + end + + step(...) +end + +local add_leaf_function +do + ---A table to store all leaf async functions + _PlenaryLeafTable = setmetatable({}, { + __mode = "k", + }) + + add_leaf_function = function(async_func, argc) + assert(_PlenaryLeafTable[async_func] == nil, "Async function should not already be in the table") + _PlenaryLeafTable[async_func] = argc + end + + function M.is_leaf_function(async_func) + return _PlenaryLeafTable[async_func] ~= nil + end + + function M.get_leaf_function_argc(async_func) + return _PlenaryLeafTable[async_func] + end +end + +---Creates an async function with a callback style function. +---@param func function: A callback style function to be converted. The last argument must be the callback. +---@param argc number: The number of arguments of func. Must be included. +---@return function: Returns an async function +M.wrap = function(func, argc) + if not is_callable(func) then + traceback_error("type error :: expected func, got " .. type(func)) + end + + if type(argc) ~= "number" then + traceback_error("type error :: expected number, got " .. type(argc)) + end + + local function leaf(...) + local nargs = select("#", ...) + + if nargs == argc then + return func(...) + else + return co.yield(func, argc, ...) + end + end + + add_leaf_function(leaf, argc) + + return leaf +end + +---Use this to either run a future concurrently and then do something else +---or use it to run a future with a callback in a non async context +---@param async_function function +---@param callback function +M.run = function(async_function, callback) + if M.is_leaf_function(async_function) then + async_function(callback) + else + execute(async_function, callback) + end +end + +---Use this to create a function which executes in an async context but +---called from a non-async context. Inherently this cannot return anything +---since it is non-blocking +---@param func function +M.void = function(func) + return function(...) + execute(func, nil, ...) + end +end + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/control.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/control.lua new file mode 100644 index 0000000..3a8a8ed --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/control.lua @@ -0,0 +1,229 @@ +local a = require "plenary.async.async" +local Deque = require("plenary.async.structs").Deque +local tbl = require "plenary.tbl" + +local M = {} + +local Condvar = {} +Condvar.__index = Condvar + +---@class Condvar +---@return Condvar +function Condvar.new() + return setmetatable({ handles = {} }, Condvar) +end + +---`blocks` the thread until a notification is received +Condvar.wait = a.wrap(function(self, callback) + -- not calling the callback will block the coroutine + table.insert(self.handles, callback) +end, 2) + +---notify everyone that is waiting on this Condvar +function Condvar:notify_all() + local len = #self.handles + for i, callback in ipairs(self.handles) do + if i > len then + -- this means that more handles were added while we were notifying + -- if we don't break we can get starvation notifying as soon as new handles are added + break + end + + callback() + end + + for _ = 1, len do + -- table.remove will ensure that indexes are correct and make "ipairs" safe, + -- which is not the case for "self.handles[i] = nil" + table.remove(self.handles) + end +end + +---notify randomly one person that is waiting on this Condvar +function Condvar:notify_one() + if #self.handles == 0 then + return + end + + local idx = math.random(#self.handles) + self.handles[idx]() + table.remove(self.handles, idx) +end + +M.Condvar = Condvar + +local Semaphore = {} +Semaphore.__index = Semaphore + +---@class Semaphore +---@param initial_permits number: the number of permits that it can give out +---@return Semaphore +function Semaphore.new(initial_permits) + vim.validate { + initial_permits = { + initial_permits, + function(n) + return n > 0 + end, + "number greater than 0", + }, + } + + return setmetatable({ permits = initial_permits, handles = {} }, Semaphore) +end + +---async function, blocks until a permit can be acquired +---example: +---local semaphore = Semaphore.new(1024) +---local permit = semaphore:acquire() +---permit:forget() +---when a permit can be acquired returns it +---call permit:forget() to forget the permit +Semaphore.acquire = a.wrap(function(self, callback) + if self.permits > 0 then + self.permits = self.permits - 1 + else + table.insert(self.handles, callback) + return + end + + local permit = {} + + permit.forget = function(self_permit) + self.permits = self.permits + 1 + + if self.permits > 0 and #self.handles > 0 then + self.permits = self.permits - 1 + table.remove(self.handles)(self_permit) + end + end + + callback(permit) +end, 2) + +M.Semaphore = Semaphore + +M.channel = {} + +---Creates a oneshot channel +---returns a sender and receiver function +---the sender is not async while the receiver is +---@return function, function +M.channel.oneshot = function() + local val = nil + local saved_callback = nil + local sent = false + local received = false + local is_single = false + + --- sender is not async + --- sends a value which can be nil + local sender = function(...) + assert(not sent, "Oneshot channel can only send once") + sent = true + + if saved_callback ~= nil then + saved_callback(...) + return + end + + -- optimise for when there is only one or zero argument, no need to pack + local nargs = select("#", ...) + if nargs == 1 or nargs == 0 then + val = ... + is_single = true + else + val = tbl.pack(...) + end + end + + --- receiver is async + --- blocks until a value is received + local receiver = a.wrap(function(callback) + assert(not received, "Oneshot channel can only receive one value!") + + if sent then + received = true + if is_single then + return callback(val) + else + return callback(tbl.unpack(val)) + end + else + saved_callback = callback + end + end, 1) + + return sender, receiver +end + +---A counter channel. +---Basically a channel that you want to use only to notify and not to send any actual values. +---@return function: sender +---@return function: receiver +M.channel.counter = function() + local counter = 0 + local condvar = Condvar.new() + + local Sender = {} + + function Sender:send() + counter = counter + 1 + condvar:notify_all() + end + + local Receiver = {} + + Receiver.recv = function() + if counter == 0 then + condvar:wait() + end + counter = counter - 1 + end + + Receiver.last = function() + if counter == 0 then + condvar:wait() + end + counter = 0 + end + + return Sender, Receiver +end + +---A multiple producer single consumer channel +---@return table +---@return table +M.channel.mpsc = function() + local deque = Deque.new() + local condvar = Condvar.new() + + local Sender = {} + + function Sender.send(...) + deque:pushleft { ... } + condvar:notify_all() + end + + local Receiver = {} + + Receiver.recv = function() + if deque:is_empty() then + condvar:wait() + end + return unpack(deque:popright()) + end + + Receiver.last = function() + if deque:is_empty() then + condvar:wait() + end + local val = deque:popleft() + deque:clear() + return unpack(val or {}) + end + + return Sender, Receiver +end + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/init.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/init.lua new file mode 100644 index 0000000..027614a --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/init.lua @@ -0,0 +1,57 @@ +---@brief [[ +--- NOTE: This API is still under construction. +--- It may change in the future :) +---@brief ]] + +local lookups = { + uv = "plenary.async.uv_async", + util = "plenary.async.util", + lsp = "plenary.async.lsp", + api = "plenary.async.api", + tests = "plenary.async.tests", + control = "plenary.async.control", +} + +local exports = setmetatable(require "plenary.async.async", { + __index = function(t, k) + local require_path = lookups[k] + if not require_path then + return + end + + local mod = require(require_path) + t[k] = mod + + return mod + end, +}) + +exports.tests.add_globals = function() + a = exports + + -- must prefix with a or stack overflow, plenary.test harness already added it + a.describe = exports.tests.describe + -- must prefix with a or stack overflow + a.it = exports.tests.it + a.pending = exports.tests.pending + a.before_each = exports.tests.before_each + a.after_each = exports.tests.after_each +end + +exports.tests.add_to_env = function() + local env = getfenv(2) + + env.a = exports + + -- must prefix with a or stack overflow, plenary.test harness already added it + env.a.describe = exports.tests.describe + -- must prefix with a or stack overflow + env.a.it = exports.tests.it + env.a.pending = exports.tests.pending + env.a.before_each = exports.tests.before_each + env.a.after_each = exports.tests.after_each + + setfenv(2, env) +end + +return exports diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/lsp.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/lsp.lua new file mode 100644 index 0000000..e76e4c4 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/lsp.lua @@ -0,0 +1,12 @@ +local a = require "plenary.async.async" + +local M = {} + +---This will be deprecated because the callback can be called multiple times. +---This will give a coroutine error because the coroutine will be resumed multiple times. +---Please use buf_request_all instead. +M.buf_request = a.wrap(vim.lsp.buf_request, 4) + +M.buf_request_all = a.wrap(vim.lsp.buf_request_all, 4) + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/structs.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/structs.lua new file mode 100644 index 0000000..c133a21 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/structs.lua @@ -0,0 +1,116 @@ +local M = {} + +local Deque = {} +Deque.__index = Deque + +---@class Deque +---A double ended queue +--- +---@return Deque +function Deque.new() + -- the indexes are created with an offset so that the indices are consequtive + -- otherwise, when both pushleft and pushright are used, the indices will have a 1 length hole in the middle + return setmetatable({ first = 0, last = -1 }, Deque) +end + +---push to the left of the deque +---@param value any +function Deque:pushleft(value) + local first = self.first - 1 + self.first = first + self[first] = value +end + +---push to the right of the deque +---@param value any +function Deque:pushright(value) + local last = self.last + 1 + self.last = last + self[last] = value +end + +---pop from the left of the deque +---@return any +function Deque:popleft() + local first = self.first + if first > self.last then + return nil + end + local value = self[first] + self[first] = nil -- to allow garbage collection + self.first = first + 1 + return value +end + +---pops from the right of the deque +---@return any +function Deque:popright() + local last = self.last + if self.first > last then + return nil + end + local value = self[last] + self[last] = nil -- to allow garbage collection + self.last = last - 1 + return value +end + +---checks if the deque is empty +---@return boolean +function Deque:is_empty() + return self:len() == 0 +end + +---returns the number of elements of the deque +---@return number +function Deque:len() + return self.last - self.first + 1 +end + +---returns and iterator of the indices and values starting from the left +---@return function +function Deque:ipairs_left() + local i = self.first + + return function() + local res = self[i] + local idx = i + + if res then + i = i + 1 + + return idx, res + end + end +end + +---returns and iterator of the indices and values starting from the right +---@return function +function Deque:ipairs_right() + local i = self.last + + return function() + local res = self[i] + local idx = i + + if res then + i = i - 1 -- advance the iterator before we return + + return idx, res + end + end +end + +---removes all values from the deque +---@return nil +function Deque:clear() + for i, _ in self:ipairs_left() do + self[i] = nil + end + self.first = 0 + self.last = -1 +end + +M.Deque = Deque + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/tests.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/tests.lua new file mode 100644 index 0000000..d10db48 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/tests.lua @@ -0,0 +1,25 @@ +local util = require "plenary.async.util" + +local M = {} + +M.describe = function(s, async_func) + describe(s, async_func) +end + +M.it = function(s, async_func) + it(s, util.will_block(async_func, tonumber(vim.env.PLENARY_TEST_TIMEOUT))) +end + +M.pending = function(async_func) + pending(async_func) +end + +M.before_each = function(async_func) + before_each(util.will_block(async_func)) +end + +M.after_each = function(async_func) + after_each(util.will_block(async_func)) +end + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/util.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/util.lua new file mode 100644 index 0000000..b80f3fd --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/util.lua @@ -0,0 +1,145 @@ +local a = require "plenary.async.async" +local vararg = require "plenary.vararg" +-- local control = a.control +local control = require "plenary.async.control" +local channel = control.channel + +local M = {} + +local defer_swapped = function(timeout, callback) + vim.defer_fn(callback, timeout) +end + +---Sleep for milliseconds +---@param ms number +M.sleep = a.wrap(defer_swapped, 2) + +---This will COMPLETELY block neovim +---please just use a.run unless you have a very special usecase +---for example, in plenary test_harness you must use this +---@param async_function Future +---@param timeout number: Stop blocking if the timeout was surpassed. Default 2000. +M.block_on = function(async_function, timeout) + async_function = M.protected(async_function) + + local stat + local ret = {} + + a.run(async_function, function(stat_, ...) + stat = stat_ + ret = { ... } + end) + + vim.wait(timeout or 2000, function() + return stat ~= nil + end, 20, false) + + if stat == false then + error(string.format("Blocking on future timed out or was interrupted.\n%s", unpack(ret))) + end + + return unpack(ret) +end + +---@see M.block_on +---@param async_function Future +---@param timeout number +M.will_block = function(async_function, timeout) + return function() + M.block_on(async_function, timeout) + end +end + +M.join = function(async_fns) + local len = #async_fns + local results = {} + if len == 0 then + return results + end + + local done = 0 + + local tx, rx = channel.oneshot() + + for i, async_fn in ipairs(async_fns) do + assert(type(async_fn) == "function", "type error :: future must be function") + + local cb = function(...) + results[i] = { ... } + done = done + 1 + if done == len then + tx() + end + end + + a.run(async_fn, cb) + end + + rx() + + return results +end + +---Returns a result from the future that finishes at the first +---@param async_functions table: The futures that you want to select +---@return ... +M.run_first = a.wrap(function(async_functions, step) + local ran = false + + for _, async_function in ipairs(async_functions) do + assert(type(async_function) == "function", "type error :: future must be function") + + local callback = function(...) + if not ran then + ran = true + step(...) + end + end + + async_function(callback) + end +end, 2) + +---Returns a result from the functions that finishes at the first +---@param funcs table: The async functions that you want to select +---@return ... +M.race = function(funcs) + local async_functions = vim.tbl_map(function(func) + return function(callback) + a.run(func, callback) + end + end, funcs) + return M.run_first(async_functions) +end + +M.run_all = function(async_fns, callback) + a.run(function() + M.join(async_fns) + end, callback) +end + +function M.apcall(async_fn, ...) + local nargs = a.get_leaf_function_argc(async_fn) + if nargs then + local tx, rx = channel.oneshot() + local stat, ret = pcall(async_fn, vararg.rotate(nargs, tx, ...)) + if not stat then + return stat, ret + else + return stat, rx() + end + else + return pcall(async_fn, ...) + end +end + +function M.protected(async_fn) + return function() + return M.apcall(async_fn) + end +end + +---An async function that when called will yield to the neovim scheduler to be able to call the api. +M.scheduler = a.wrap(vim.schedule, 1) + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/uv_async.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/uv_async.lua new file mode 100644 index 0000000..427e1a5 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async/uv_async.lua @@ -0,0 +1,84 @@ +local a = require "plenary.async.async" +local uv = vim.loop + +local M = {} + +local function add(name, argc, custom) + local success, ret = pcall(a.wrap, custom or uv[name], argc) + + if not success then + error("Failed to add function with name " .. name) + end + + M[name] = ret +end + +add("close", 4) -- close a handle + +-- filesystem operations +add("fs_open", 4) +add("fs_read", 4) +add("fs_close", 2) +add("fs_unlink", 2) +add("fs_write", 4) +add("fs_mkdir", 3) +add("fs_mkdtemp", 2) +-- 'fs_mkstemp', +add("fs_rmdir", 2) +add("fs_scandir", 2) +add("fs_stat", 2) +add("fs_fstat", 2) +add("fs_lstat", 2) +add("fs_rename", 3) +add("fs_fsync", 2) +add("fs_fdatasync", 2) +add("fs_ftruncate", 3) +add("fs_sendfile", 5) +add("fs_access", 3) +add("fs_chmod", 3) +add("fs_fchmod", 3) +add("fs_utime", 4) +add("fs_futime", 4) +-- 'fs_lutime', +add("fs_link", 3) +add("fs_symlink", 4) +add("fs_readlink", 2) +add("fs_realpath", 2) +add("fs_chown", 4) +add("fs_fchown", 4) +-- 'fs_lchown', +add("fs_copyfile", 4) +add("fs_opendir", 3, function(path, entries, callback) + return uv.fs_opendir(path, callback, entries) +end) +add("fs_readdir", 2) +add("fs_closedir", 2) +-- 'fs_statfs', + +-- stream +add("shutdown", 2) +add("listen", 3) +-- add('read_start', 2) -- do not do this one, the callback is made multiple times +add("write", 3) +add("write2", 4) +add("shutdown", 2) + +-- tcp +add("tcp_connect", 4) +-- 'tcp_close_reset', + +-- pipe +add("pipe_connect", 3) + +-- udp +add("udp_send", 5) +add("udp_recv_start", 2) + +-- fs event (wip make into async await event) +-- fs poll event (wip make into async await event) + +-- dns +add("getaddrinfo", 4) +add("getnameinfo", 2) + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/api.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/api.lua new file mode 100644 index 0000000..baaf80c --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/api.lua @@ -0,0 +1,15 @@ +local a = require "plenary.async_lib.async" +local async, await = a.async, a.await + +return setmetatable({}, { + __index = function(t, k) + return async(function(...) + -- if we are in a fast event await the scheduler + if vim.in_fast_event() then + await(a.scheduler()) + end + + vim.api[k](...) + end) + end, +}) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/async.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/async.lua new file mode 100644 index 0000000..84ca70d --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/async.lua @@ -0,0 +1,213 @@ +local co = coroutine +local errors = require "plenary.errors" +local traceback_error = errors.traceback_error +local f = require "plenary.functional" +local tbl = require "plenary.tbl" + +local M = {} + +---because we can't store varargs +local function callback_or_next(step, thread, callback, ...) + local stat = f.first(...) + + if not stat then + error(string.format("The coroutine failed with this message: %s", f.second(...))) + end + + if co.status(thread) == "dead" then + (callback or function() end)(select(2, ...)) + else + assert(select("#", select(2, ...)) == 1, "expected a single return value") + local returned_future = f.second(...) + assert(type(returned_future) == "function", "type error :: expected func") + returned_future(step) + end +end + +---@class Future +---Something that will give a value when run + +---Executes a future with a callback when it is done +---@param future Future: the future to execute +---@param callback function: the callback to call when done +local execute = function(future, callback) + assert(type(future) == "function", "type error :: expected func") + local thread = co.create(future) + + local step + step = function(...) + callback_or_next(step, thread, callback, co.resume(thread, ...)) + end + + step() +end + +---Creates an async function with a callback style function. +---@param func function: A callback style function to be converted. The last argument must be the callback. +---@param argc number: The number of arguments of func. Must be included. +---@return function: Returns an async function +M.wrap = function(func, argc) + if type(func) ~= "function" then + traceback_error("type error :: expected func, got " .. type(func)) + end + + if type(argc) ~= "number" and argc ~= "vararg" then + traceback_error "expected argc to be a number or string literal 'vararg'" + end + + return function(...) + local params = tbl.pack(...) + + local function future(step) + if step then + if type(argc) == "number" then + params[argc] = step + params.n = argc + else + table.insert(params, step) -- change once not optional + params.n = params.n + 1 + end + + return func(tbl.unpack(params)) + else + return co.yield(future) + end + end + return future + end +end + +---Return a new future that when run will run all futures concurrently. +---@param futures table: the futures that you want to join +---@return Future: returns a future +M.join = M.wrap(function(futures, step) + local len = #futures + local results = {} + local done = 0 + + if len == 0 then + return step(results) + end + + for i, future in ipairs(futures) do + assert(type(future) == "function", "type error :: future must be function") + + local callback = function(...) + results[i] = { ... } + done = done + 1 + if done == len then + step(results) + end + end + + future(callback) + end +end, 2) + +---Returns a future that when run will select the first future that finishes +---@param futures table: The future that you want to select +---@return Future +M.select = M.wrap(function(futures, step) + local selected = false + + for _, future in ipairs(futures) do + assert(type(future) == "function", "type error :: future must be function") + + local callback = function(...) + if not selected then + selected = true + step(...) + end + end + + future(callback) + end +end, 2) + +---Use this to either run a future concurrently and then do something else +---or use it to run a future with a callback in a non async context +---@param future Future +---@param callback function +M.run = function(future, callback) + future(callback or function() end) +end + +---Same as run but runs multiple futures +---@param futures table +---@param callback function +M.run_all = function(futures, callback) + M.run(M.join(futures), callback) +end + +---Await a future, yielding the current function +---@param future Future +---@return any: returns the result of the future when it is done +M.await = function(future) + assert(type(future) == "function", "type error :: expected function to await") + return future(nil) +end + +---Same as await but can await multiple futures. +---If the futures have libuv leaf futures they will be run concurrently +---@param futures table +---@return table: returns a table of results that each future returned. Note that if the future returns multiple values they will be packed into a table. +M.await_all = function(futures) + assert(type(futures) == "table", "type error :: expected table") + return M.await(M.join(futures)) +end + +---suspend a coroutine +M.suspend = co.yield + +---create a async scope +M.scope = function(func) + M.run(M.future(func)) +end + +--- Future a :: a -> (a -> ()) +--- turns this signature +--- ... -> Future a +--- into this signature +--- ... -> () +M.void = function(async_func) + return function(...) + async_func(...)(function() end) + end +end + +M.async_void = function(func) + return M.void(M.async(func)) +end + +---creates an async function +---@param func function +---@return function: returns an async function +M.async = function(func) + if type(func) ~= "function" then + traceback_error("type error :: expected func, got " .. type(func)) + end + + return function(...) + local args = tbl.pack(...) + local function future(step) + if step == nil then + return func(tbl.unpack(args)) + else + execute(future, step) + end + end + return future + end +end + +---creates a future +---@param func function +---@return Future +M.future = function(func) + return M.async(func)() +end + +---An async function that when awaited will await the scheduler to be able to call the api. +M.scheduler = M.wrap(vim.schedule, 1) + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/init.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/init.lua new file mode 100644 index 0000000..4f1ce55 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/init.lua @@ -0,0 +1,41 @@ +---@brief [[ +--- NOTE: This API is still under construction. +--- It may change in the future :) +---@brief ]] + +local exports = require "plenary.async_lib.async" +exports.uv = require "plenary.async_lib.uv_async" +exports.util = require "plenary.async_lib.util" +exports.lsp = require "plenary.async_lib.lsp" +exports.api = require "plenary.async_lib.api" +exports.tests = require "plenary.async_lib.tests" + +exports.tests.add_globals = function() + a = exports + async = exports.async + await = exports.await + await_all = exports.await_all + + -- must prefix with a or stack overflow, plenary.test harness already added it + a.describe = exports.tests.describe + -- must prefix with a or stack overflow + a.it = exports.tests.it +end + +exports.tests.add_to_env = function() + local env = getfenv(2) + + env.a = exports + env.async = exports.async + env.await = exports.await + env.await_all = exports.await_all + + -- must prefix with a or stack overflow, plenary.test harness already added it + env.a.describe = exports.tests.describe + -- must prefix with a or stack overflow + env.a.it = exports.tests.it + + setfenv(2, env) +end + +return exports diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/lsp.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/lsp.lua new file mode 100644 index 0000000..924a763 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/lsp.lua @@ -0,0 +1,15 @@ +local a = require "plenary.async_lib.async" + +local M = {} + +---This will be deprecated because the callback can be called multiple times. +---This will give a coroutine error because the coroutine will be resumed multiple times. +---Please use buf_request_all instead. +M.buf_request = a.wrap(vim.lsp.buf_request, 4) + +--This was recently merged into master so we just check if it is there +if vim.lsp.buf_request_all ~= nil then + M.buf_request_all = a.wrap(vim.lsp.buf_request_all, 4) +end + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/structs.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/structs.lua new file mode 100644 index 0000000..73252e4 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/structs.lua @@ -0,0 +1,116 @@ +local M = {} + +Deque = {} +Deque.__index = Deque + +---@class Deque +---A double ended queue +--- +---@return Deque +function Deque.new() + -- the indexes are created with an offset so that the indices are consequtive + -- otherwise, when both pushleft and pushright are used, the indices will have a 1 length hole in the middle + return setmetatable({ first = 0, last = -1 }, Deque) +end + +---push to the left of the deque +---@param value any +function Deque:pushleft(value) + local first = self.first - 1 + self.first = first + self[first] = value +end + +---push to the right of the deque +---@param value any +function Deque:pushright(value) + local last = self.last + 1 + self.last = last + self[last] = value +end + +---pop from the left of the deque +---@return any +function Deque:popleft() + local first = self.first + if first > self.last then + return nil + end + local value = self[first] + self[first] = nil -- to allow garbage collection + self.first = first + 1 + return value +end + +---pops from the right of the deque +---@return any +function Deque:popright() + local last = self.last + if self.first > last then + return nil + end + local value = self[last] + self[last] = nil -- to allow garbage collection + self.last = last - 1 + return value +end + +---checks if the deque is empty +---@return boolean +function Deque:is_empty() + return self:len() == 0 +end + +---returns the number of elements of the deque +---@return number +function Deque:len() + return self.last - self.first + 1 +end + +---returns and iterator of the indices and values starting from the left +---@return function +function Deque:ipairs_left() + local i = self.first + + return function() + local res = self[i] + local idx = i + + if res then + i = i + 1 + + return idx, res + end + end +end + +---returns and iterator of the indices and values starting from the right +---@return function +function Deque:ipairs_right() + local i = self.last + + return function() + local res = self[i] + local idx = i + + if res then + i = i - 1 -- advance the iterator before we return + + return idx, res + end + end +end + +---removes all values from the deque +---@return nil +function Deque:clear() + for i, _ in self:ipairs_left() do + self[i] = nil + end + self.first = 0 + self.last = -1 +end + +M.Deque = Deque + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/tests.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/tests.lua new file mode 100644 index 0000000..8dff05b --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/tests.lua @@ -0,0 +1,14 @@ +local a = require "plenary.async_lib.async" +local util = require "plenary.async_lib.util" + +local M = {} + +M.describe = function(s, func) + describe(s, util.will_block(a.future(func))) +end + +M.it = function(s, func) + it(s, util.will_block(a.future(func))) +end + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/util.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/util.lua new file mode 100644 index 0000000..1de65fe --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/util.lua @@ -0,0 +1,339 @@ +local a = require "plenary.async_lib.async" +local await = a.await +local async = a.async +local co = coroutine +local Deque = require("plenary.async_lib.structs").Deque +local uv = vim.loop + +local M = {} + +---Sleep for milliseconds +---@param ms number +M.sleep = a.wrap(function(ms, callback) + local timer = uv.new_timer() + uv.timer_start(timer, ms, 0, function() + uv.timer_stop(timer) + uv.close(timer) + callback() + end) +end, 2) + +---Takes a future and a millisecond as the timeout. +---If the time is reached and the future hasn't completed yet, it will short circuit the future +---NOTE: the future will still be running in libuv, we are just not waiting for it to complete +---thats why you should call this on a leaf future only to avoid unexpected results +---@param future Future +---@param ms number +M.timeout = a.wrap(function(future, ms, callback) + -- make sure that the callback isn't called twice, or else the coroutine can be dead + local done = false + + local timeout_callback = function(...) + if not done then + done = true + callback(false, ...) -- false because it has run normally + end + end + + vim.defer_fn(function() + if not done then + done = true + callback(true) -- true because it has timed out + end + end, ms) + + a.run(future, timeout_callback) +end, 3) + +---create an async function timer +---@param ms number +M.timer = function(ms) + return async(function() + await(M.sleep(ms)) + end) +end + +---id function that can be awaited +---@param nil ... +---@return ... +M.id = async(function(...) + return ... +end) + +---Running this function will yield now and do nothing else +M.yield_now = async(function() + await(M.id()) +end) + +local Condvar = {} +Condvar.__index = Condvar + +---@class Condvar +---@return Condvar +function Condvar.new() + return setmetatable({ handles = {} }, Condvar) +end + +---`blocks` the thread until a notification is received +Condvar.wait = a.wrap(function(self, callback) + -- not calling the callback will block the coroutine + table.insert(self.handles, callback) +end, 2) + +---notify everyone that is waiting on this Condvar +function Condvar:notify_all() + if #self.handles == 0 then + return + end + + for i, callback in ipairs(self.handles) do + callback() + self.handles[i] = nil + end +end + +---notify randomly one person that is waiting on this Condvar +function Condvar:notify_one() + if #self.handles == 0 then + return + end + + local idx = math.random(#self.handles) + self.handles[idx]() + table.remove(self.handles, idx) +end + +M.Condvar = Condvar + +local Semaphore = {} +Semaphore.__index = Semaphore + +---@class Semaphore +---@param initial_permits number: the number of permits that it can give out +---@return Semaphore +function Semaphore.new(initial_permits) + vim.validate { + initial_permits = { + initial_permits, + function(n) + return n > 0 + end, + "number greater than 0", + }, + } + + return setmetatable({ permits = initial_permits, handles = {} }, Semaphore) +end + +---async function, blocks until a permit can be acquired +---example: +---local semaphore = Semaphore.new(1024) +---local permit = await(semaphore:acquire()) +---permit:forget() +---when a permit can be acquired returns it +---call permit:forget() to forget the permit +Semaphore.acquire = a.wrap(function(self, callback) + if self.permits > 0 then + self.permits = self.permits - 1 + else + table.insert(self.handles, callback) + return + end + + local permit = {} + + permit.forget = function(self_permit) + self.permits = self.permits + 1 + + if self.permits > 0 and #self.handles > 0 then + self.permits = self.permits - 1 + local callback = table.remove(self.handles) + callback(self_permit) + end + end + + callback(permit) +end, 2) + +M.Semaphore = Semaphore + +M.channel = {} + +---Creates a oneshot channel +---returns a sender and receiver function +---the sender is not async while the receiver is +---@return function, function +M.channel.oneshot = function() + local val = nil + local saved_callback = nil + local sent = false + local received = false + + --- sender is not async + --- sends a value + local sender = function(...) + if sent then + error "Oneshot channel can only send once" + end + + sent = true + + local args = { ... } + + if saved_callback then + saved_callback(unpack(val or args)) + else + val = args + end + end + + --- receiver is async + --- blocks until a value is received + local receiver = a.wrap(function(callback) + if received then + error "Oneshot channel can only send one value!" + end + + if val then + received = true + callback(unpack(val)) + else + saved_callback = callback + end + end, 1) + + return sender, receiver +end + +---A counter channel. +---Basically a channel that you want to use only to notify and not to send any actual values. +---@return function: sender +---@return function: receiver +M.channel.counter = function() + local counter = 0 + local condvar = Condvar.new() + + local Sender = {} + + function Sender:send() + counter = counter + 1 + condvar:notify_all() + end + + local Receiver = {} + + Receiver.recv = async(function() + if counter == 0 then + await(condvar:wait()) + end + counter = counter - 1 + end) + + Receiver.last = async(function() + if counter == 0 then + await(condvar:wait()) + end + counter = 0 + end) + + return Sender, Receiver +end + +---A multiple producer single consumer channel +---@return table +---@return table +M.channel.mpsc = function() + local deque = Deque.new() + local condvar = Condvar.new() + + local Sender = {} + + function Sender.send(...) + deque:pushleft { ... } + condvar:notify_all() + end + + local Receiver = {} + + Receiver.recv = async(function() + if deque:is_empty() then + await(condvar:wait()) + end + return unpack(deque:popright()) + end) + + Receiver.last = async(function() + if deque:is_empty() then + await(condvar:wait()) + end + local val = deque:popright() + deque:clear() + return unpack(val) + end) + + return Sender, Receiver +end + +local pcall_wrap = function(func) + return function(...) + return pcall(func, ...) + end +end + +---Makes a future protected. It is like pcall but for futures. +---Only works for non-leaf futures +M.protected_non_leaf = async(function(future) + return await(pcall_wrap(future)) +end) + +---Makes a future protected. It is like pcall but for futures. +---@param future Future +---@return Future +M.protected = async(function(future) + local tx, rx = M.channel.oneshot() + + stat, ret = pcall(future, tx) + + if stat == true then + return stat, await(rx()) + else + return stat, ret + end +end) + +---This will COMPLETELY block neovim +---please just use a.run unless you have a very special usecase +---for example, in plenary test_harness you must use this +---@param future Future +---@param timeout number: Stop blocking if the timeout was surpassed. Default 2000. +M.block_on = function(future, timeout) + future = M.protected(future) + + local stat, ret + a.run(future, function(_stat, ...) + stat = _stat + ret = { ... } + end) + + local function check() + if stat == false then + error("Blocking on future failed " .. unpack(ret)) + end + return stat == true + end + + if not vim.wait(timeout or 2000, check, 20, false) then + error "Blocking on future timed out or was interrupted" + end + + return unpack(ret) +end + +---Returns a new future that WILL BLOCK +---@param future Future +---@return Future +M.will_block = async(function(future) + return M.block_on(future) +end) + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/uv_async.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/uv_async.lua new file mode 100644 index 0000000..4ff4ef6 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/async_lib/uv_async.lua @@ -0,0 +1,82 @@ +local a = require "plenary.async_lib.async" +local uv = vim.loop + +local M = {} + +local function add(name, argc) + local success, ret = pcall(a.wrap, uv[name], argc) + + if not success then + error("Failed to add function with name " .. name) + end + + M[name] = ret +end + +add("close", 4) -- close a handle + +-- filesystem operations +add("fs_open", 4) +add("fs_read", 4) +add("fs_close", 2) +add("fs_unlink", 2) +add("fs_write", 4) +add("fs_mkdir", 3) +add("fs_mkdtemp", 2) +-- 'fs_mkstemp', +add("fs_rmdir", 2) +add("fs_scandir", 2) +add("fs_stat", 2) +add("fs_fstat", 2) +add("fs_lstat", 2) +add("fs_rename", 3) +add("fs_fsync", 2) +add("fs_fdatasync", 2) +add("fs_ftruncate", 3) +add("fs_sendfile", 5) +add("fs_access", 3) +add("fs_chmod", 3) +add("fs_fchmod", 3) +add("fs_utime", 4) +add("fs_futime", 4) +-- 'fs_lutime', +add("fs_link", 3) +add("fs_symlink", 4) +add("fs_readlink", 2) +add("fs_realpath", 2) +add("fs_chown", 4) +add("fs_fchown", 4) +-- 'fs_lchown', +add("fs_copyfile", 4) +-- add('fs_opendir', 3) -- TODO: fix this one +add("fs_readdir", 2) +add("fs_closedir", 2) +-- 'fs_statfs', + +-- stream +add("shutdown", 2) +add("listen", 3) +-- add('read_start', 2) -- do not do this one, the callback is made multiple times +add("write", 3) +add("write2", 4) +add("shutdown", 2) + +-- tcp +add("tcp_connect", 4) +-- 'tcp_close_reset', + +-- pipe +add("pipe_connect", 3) + +-- udp +add("udp_send", 5) +add("udp_recv_start", 2) + +-- fs event (wip make into async await event) +-- fs poll event (wip make into async await event) + +-- dns +add("getaddrinfo", 4) +add("getnameinfo", 2) + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/benchmark/init.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/benchmark/init.lua new file mode 100644 index 0000000..ec32940 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/benchmark/init.lua @@ -0,0 +1,125 @@ +local stat = require "plenary.benchmark.stat" + +local get_stats = function(results) + local ret = {} + + ret.max, ret.min = stat.maxmin(results) + ret.mean = stat.mean(results) + ret.median = stat.median(results) + ret.std = stat.std_dev(results) + + return ret +end + +local get_output = function(index, res, runs) + -- divine with a sutable one / 1e3, 1e6, 1e9 + local time_types = { "ns", "μs", "ms" } + + local get_leading = function(time) + time = math.floor(time) + local count = 0 + repeat + time = math.floor(time / 10) + count = count + 1 + until time <= 0 + return count + end + + local get_best_fmt = function(time) + for _, v in ipairs(time_types) do + if math.abs(time) < 1000.0 then + return string.format("%s%3.1f %s", string.rep(" ", 3 - get_leading(time)), time, v) + end + time = time / 1000.0 + end + return string.format("%.1f %s", time, "s") + end + + return string.format( + "Benchmark #%d: '%s'\n Time(mean ± σ): %s ± %s\n Range(min … max): %s … %s %d runs\n", + index, + res.name, + get_best_fmt(res.stats.mean), + get_best_fmt(res.stats.std), + get_best_fmt(res.stats.min), + get_best_fmt(res.stats.max), + runs + ) +end + +local get_summary = function(res) + if #res == 1 then + return "" + end + + local fastest_mean = math.huge + local fastest_index = 1 + for i, benchmark in ipairs(res) do + if benchmark.stats.mean < fastest_mean then + fastest_mean = benchmark.stats.mean + fastest_index = i + end + end + + if fastest_mean == math.huge then + return "" + end + + local output = {} + local fastest = res[fastest_index].stats + for i, benchmark in ipairs(res) do + if i ~= fastest_index then + local result = benchmark.stats + local ratio = result.mean / fastest.mean + + -- // https://en.wikipedia.org/wiki/Propagation_of_uncertainty#Example_formulas + -- // Covariance asssumed to be 0, i.e. variables are assumed to be independent + local ratio_std = ratio + * math.sqrt(math.pow(result.std / result.mean, 2) + math.pow(fastest.std / fastest.mean, 2)) + + table.insert(output, string.format(" %.1f ± %.1f times faster than '%s'\n", ratio, ratio_std, benchmark.name)) + end + end + + return string.format("Summary\n '%s' ran\n%s", res[fastest_index].name, table.concat(output, "")) +end + +---@class benchmark_run_opts +---@field warmup number @number of initial runs before starting to track time. +---@field runs number @number of runs to make +---@field fun table> @functions to execute + +---Benchmark a function +---@param name string @benchmark name +---@param opts benchmark_run_opts +local bench = function(name, opts) + vim.validate { + opts = { opts, "table" }, + fun = { opts.fun, "table" }, + } + opts.warmup = vim.F.if_nil(opts.warmup, 3) + opts.runs = vim.F.if_nil(opts.runs, 5) + + opts.fun = type(opts.fun) == "function" and { opts.fun } or opts.fun + local output = { string.format("Benchmark Group: '%s' -----------------------\n", name) } + local res = {} + for i, fun in ipairs(opts.fun) do + res[i] = { name = fun[1], results = {} } + for _ = 1, opts.warmup do + fun[2]() + end + for j = 1, opts.runs do + local start = vim.loop.hrtime() + fun[2]() + res[i].results[j] = vim.loop.hrtime() - start + end + res[i].stats = get_stats(res[i].results) + table.insert(output, get_output(i, res[i], opts.runs)) + end + + print(string.format("%s\n%s", table.concat(output, ""), get_summary(res))) + + return res +end + +return bench diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/benchmark/stat.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/benchmark/stat.lua new file mode 100644 index 0000000..63c1f6b --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/benchmark/stat.lua @@ -0,0 +1,86 @@ +local stat = {} + +---Calculate mean +---@param t number[] @double +---@return number @double +stat.mean = function(t) + local sum = 0 + local count = 0 + + for _, v in pairs(t) do + if type(v) == "number" then + sum = sum + v + count = count + 1 + end + end + + return (sum / count) +end + +-- Get the median of a table. +---@param t number[] +---@return number +stat.median = function(t) + local temp = {} + + -- deep copy table so that when we sort it, the original is unchanged + -- also weed out any non numbers + for _, v in pairs(t) do + if type(v) == "number" then + table.insert(temp, v) + end + end + + table.sort(temp) + + -- If we have an even number of table elements or odd. + if math.fmod(#temp, 2) == 0 then + -- return mean value of middle two elements + return (temp[#temp / 2] + temp[(#temp / 2) + 1]) / 2 + else + -- return middle element + return temp[math.ceil(#temp / 2)] + end +end + +--- Get the standard deviation of a table +---@param t number[] +stat.std_dev = function(t) + local m, vm, result + local sum = 0 + local count = 0 + + m = stat.mean(t) + + for _, v in pairs(t) do + if type(v) == "number" then + vm = v - m + sum = sum + (vm * vm) + count = count + 1 + end + end + + result = math.sqrt(sum / (count - 1)) + + return result +end + +---Get the max and min for a table +---@param t number[] +---@return number +---@return number +stat.maxmin = function(t) + local max = -math.huge + local min = math.huge + + for _, v in pairs(t) do + if type(v) == "number" then + max = math.max(max, v) + min = math.min(min, v) + end + end + + return max, min +end + +return stat diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/bit.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/bit.lua new file mode 100644 index 0000000..68a6091 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/bit.lua @@ -0,0 +1,339 @@ +-- Shortcircuit to returning bit if it already exists +if bit then return bit end + +--[[ + +Credit: https://github.com/davidm/lua-bit-numberlua/blob/master/lmod/bit/numberlua.lua + +LUA MODULE + + bit.numberlua - Bitwise operations implemented in pure Lua as numbers, + with Lua 5.2 'bit32' and (LuaJIT) LuaBitOp 'bit' compatibility interfaces. + +SYNOPSIS + + local bit = require 'bit.numberlua' + print(bit.band(0xff00ff00, 0x00ff00ff)) --> 0xffffffff + + -- Interface providing strong (LuaJIT) LuaBitOp 'bit' compatibility + local bit = require 'plenary.bit' + assert(bit.tobit(0xffffffff) == -1) + + REMOVED! + -- Interface providing strong Lua 5.2 'bit32' compatibility + local bit32 = require 'bit.numberlua'.bit32 + assert(bit32.band(-1) == 0xffffffff) + + +DESCRIPTION + + This library implements bitwise operations entirely in Lua. + This module is typically intended if for some reasons you don't want + to or cannot install a popular C based bit library like BitOp 'bit' [1] + (which comes pre-installed with LuaJIT) or 'bit32' (which comes + pre-installed with Lua 5.2) but want a similar interface. + + This modules represents bit arrays as non-negative Lua numbers. [1] + It can represent 32-bit bit arrays when Lua is compiled + with lua_Number as double-precision IEEE 754 floating point. + + The module is nearly the most efficient it can be but may be a few times + slower than the C based bit libraries and is orders or magnitude + slower than LuaJIT bit operations, which compile to native code. Therefore, + this library is inferior in performane to the other modules. + + The `xor` function in this module is based partly on Roberto Ierusalimschy's + post in http://lua-users.org/lists/lua-l/2002-09/msg00134.html . + + The included BIT.bit32 and BIT.bit sublibraries aims to provide 100% + compatibility with the Lua 5.2 "bit32" and (LuaJIT) LuaBitOp "bit" library. + This compatbility is at the cost of some efficiency since inputted + numbers are normalized and more general forms (e.g. multi-argument + bitwise operators) are supported. + +STATUS + + WARNING: Not all corner cases have been tested and documented. + Some attempt was made to make these similar to the Lua 5.2 [2] + and LuaJit BitOp [3] libraries, but this is not fully tested and there + are currently some differences. Addressing these differences may + be improved in the future but it is not yet fully determined how to + resolve these differences. + + The BIT.bit32 library passes the Lua 5.2 test suite (bitwise.lua) + http://www.lua.org/tests/5.2/ . The BIT.bit library passes the LuaBitOp + test suite (bittest.lua). However, these have not been tested on + platforms with Lua compiled with 32-bit integer numbers. + +API + + Module's return + + This table contains functions that aim to provide 100% compatibility + with the LuaBitOp "bit" library (from LuaJIT). + + bit.tobit(x) --> y + bit.tohex(x [,n]) --> y + bit.bnot(x) --> y + bit.bor(x1 [,x2...]) --> y + bit.band(x1 [,x2...]) --> y + bit.bxor(x1 [,x2...]) --> y + bit.lshift(x, n) --> y + bit.rshift(x, n) --> y + bit.arshift(x, n) --> y + bit.rol(x, n) --> y + bit.ror(x, n) --> y + bit.bswap(x) --> y + +DEPENDENCIES + + None (other than Lua 5.1 or 5.2). + +REFERENCES + + [1] http://lua-users.org/wiki/FloatingPoint + [2] http://www.lua.org/manual/5.2/ + [3] http://bitop.luajit.org/ + +LICENSE + + (c) 2008-2011 David Manura. Licensed under the same terms as Lua (MIT). + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + (end license) + + Some modifications by plenary team. + +--]] + +local M = {_TYPE='module', _NAME='bit.numberlua', _VERSION='0.3.1.20120131'} + +local floor = math.floor + +local MOD = 2^32 +local MODM = MOD-1 + +local function memoize(f) + local mt = {} + local t = setmetatable({}, mt) + function mt:__index(k) + local v = f(k); t[k] = v + return v + end + return t +end + +local function make_bitop_uncached(t, m) + local function bitop(a, b) + local res,p = 0,1 + while a ~= 0 and b ~= 0 do + local am, bm = a%m, b%m + res = res + t[am][bm]*p + a = (a - am) / m + b = (b - bm) / m + p = p*m + end + res = res + (a+b)*p + return res + end + return bitop +end + +local function make_bitop(t) + local op1 = make_bitop_uncached(t,2^1) + local op2 = memoize(function(a) + return memoize(function(b) + return op1(a, b) + end) + end) + return make_bitop_uncached(op2, 2^(t.n or 1)) +end + +-- ok? probably not if running on a 32-bit int Lua number type platform +function M.tobit(x) + return x % 2^32 +end + +M.bxor = make_bitop {[0]={[0]=0,[1]=1},[1]={[0]=1,[1]=0}, n=4} +local bxor = M.bxor + +function M.bnot(a) return MODM - a end +local bnot = M.bnot + +function M.band(a,b) return ((a+b) - bxor(a,b))/2 end +local band = M.band + +function M.bor(a,b) return MODM - band(MODM - a, MODM - b) end +local bor = M.bor + +local lshift, rshift -- forward declare + +function M.rshift(a,disp) -- Lua5.2 insipred + if disp < 0 then return lshift(a,-disp) end + return floor(a % 2^32 / 2^disp) +end +rshift = M.rshift + +function M.lshift(a,disp) -- Lua5.2 inspired + if disp < 0 then return rshift(a,-disp) end + return (a * 2^disp) % 2^32 +end +lshift = M.lshift + +function M.tohex(x, n) -- BitOp style + n = n or 8 + local up + if n <= 0 then + if n == 0 then return '' end + up = true + n = - n + end + x = band(x, 16^n-1) + return ('%0'..n..(up and 'X' or 'x')):format(x) +end +local tohex = M.tohex + +function M.extract(n, field, width) -- Lua5.2 inspired + width = width or 1 + return band(rshift(n, field), 2^width-1) +end + +function M.replace(n, v, field, width) -- Lua5.2 inspired + width = width or 1 + local mask1 = 2^width-1 + v = band(v, mask1) -- required by spec? + local mask = bnot(lshift(mask1, field)) + return band(n, mask) + lshift(v, field) +end + +function M.bswap(x) -- BitOp style + local a = band(x, 0xff); x = rshift(x, 8) + local b = band(x, 0xff); x = rshift(x, 8) + local c = band(x, 0xff); x = rshift(x, 8) + local d = band(x, 0xff) + return lshift(lshift(lshift(a, 8) + b, 8) + c, 8) + d +end +local bswap = M.bswap + +function M.rrotate(x, disp) -- Lua5.2 inspired + disp = disp % 32 + local low = band(x, 2^disp-1) + return rshift(x, disp) + lshift(low, 32-disp) +end +local rrotate = M.rrotate + +function M.lrotate(x, disp) -- Lua5.2 inspired + return rrotate(x, -disp) +end +local lrotate = M.lrotate + +M.rol = M.lrotate -- LuaOp inspired +M.ror = M.rrotate -- LuaOp insipred + + +function M.arshift(x, disp) -- Lua5.2 inspired + local z = rshift(x, disp) + if x >= 0x80000000 then z = z + lshift(2^disp-1, 32-disp) end + return z +end +local arshift = M.arshift + +function M.btest(x, y) -- Lua5.2 inspired + return band(x, y) ~= 0 +end + +-- +-- Start LuaBitOp "bit" compat section. +-- + +M.bit = {} -- LuaBitOp "bit" compatibility + +function M.bit.tobit(x) + x = x % MOD + if x >= 0x80000000 then x = x - MOD end + return x +end +local bit_tobit = M.bit.tobit + +function M.bit.tohex(x, ...) + return tohex(x % MOD, ...) +end + +function M.bit.bnot(x) + return bit_tobit(bnot(x % MOD)) +end + +local function bit_bor(a, b, c, ...) + if c then + return bit_bor(bit_bor(a, b), c, ...) + elseif b then + return bit_tobit(bor(a % MOD, b % MOD)) + else + return bit_tobit(a) + end +end +M.bit.bor = bit_bor + +local function bit_band(a, b, c, ...) + if c then + return bit_band(bit_band(a, b), c, ...) + elseif b then + return bit_tobit(band(a % MOD, b % MOD)) + else + return bit_tobit(a) + end +end +M.bit.band = bit_band + +local function bit_bxor(a, b, c, ...) + if c then + return bit_bxor(bit_bxor(a, b), c, ...) + elseif b then + return bit_tobit(bxor(a % MOD, b % MOD)) + else + return bit_tobit(a) + end +end +M.bit.bxor = bit_bxor + +function M.bit.lshift(x, n) + return bit_tobit(lshift(x % MOD, n % 32)) +end + +function M.bit.rshift(x, n) + return bit_tobit(rshift(x % MOD, n % 32)) +end + +function M.bit.arshift(x, n) + return bit_tobit(arshift(x % MOD, n % 32)) +end + +function M.bit.rol(x, n) + return bit_tobit(lrotate(x % MOD, n % 32)) +end + +function M.bit.ror(x, n) + return bit_tobit(rrotate(x % MOD, n % 32)) +end + +function M.bit.bswap(x) + return bit_tobit(bswap(x % MOD)) +end + +return M.bit diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/busted.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/busted.lua new file mode 100644 index 0000000..ed1b68d --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/busted.lua @@ -0,0 +1,271 @@ +local dirname = function(p) + return vim.fn.fnamemodify(p, ":h") +end + +local function get_trace(element, level, msg) + local function trimTrace(info) + local index = info.traceback:find "\n%s*%[C]" + info.traceback = info.traceback:sub(1, index) + return info + end + level = level or 3 + + local thisdir = dirname(debug.getinfo(1, "Sl").source, ":h") + local info = debug.getinfo(level, "Sl") + while + info.what == "C" + or info.short_src:match "luassert[/\\].*%.lua$" + or (info.source:sub(1, 1) == "@" and thisdir == dirname(info.source)) + do + level = level + 1 + info = debug.getinfo(level, "Sl") + end + + info.traceback = debug.traceback("", level) + info.message = msg + + -- local file = busted.getFile(element) + local file = false + return file and file.getTrace(file.name, info) or trimTrace(info) +end + +local is_headless = require("plenary.nvim_meta").is_headless + +-- We are shadowing print so people can reliably print messages +print = function(...) + for _, v in ipairs { ... } do + io.stdout:write(tostring(v)) + io.stdout:write "\t" + end + + io.stdout:write "\r\n" +end + +local mod = {} + +local results = {} +local current_description = {} +local current_before_each = {} +local current_after_each = {} + +local add_description = function(desc) + table.insert(current_description, desc) + + return vim.deepcopy(current_description) +end + +local pop_description = function() + current_description[#current_description] = nil +end + +local add_new_each = function() + current_before_each[#current_description] = {} + current_after_each[#current_description] = {} +end + +local clear_last_each = function() + current_before_each[#current_description] = nil + current_after_each[#current_description] = nil +end + +local call_inner = function(desc, func) + local desc_stack = add_description(desc) + add_new_each() + local ok, msg = xpcall(func, function(msg) + -- debug.traceback + -- return vim.inspect(get_trace(nil, 3, msg)) + local trace = get_trace(nil, 3, msg) + return trace.message .. "\n" .. trace.traceback + end) + clear_last_each() + pop_description() + + return ok, msg, desc_stack +end + +local color_table = { + yellow = 33, + green = 32, + red = 31, +} + +local color_string = function(color, str) + if not is_headless then + return str + end + + return string.format("%s[%sm%s%s[%sm", string.char(27), color_table[color] or 0, str, string.char(27), 0) +end + +local SUCCESS = color_string("green", "Success") +local FAIL = color_string("red", "Fail") +local PENDING = color_string("yellow", "Pending") + +local HEADER = string.rep("=", 40) + +mod.format_results = function(res) + print "" + print(color_string("green", "Success: "), #res.pass) + print(color_string("red", "Failed : "), #res.fail) + print(color_string("red", "Errors : "), #res.errs) + print(HEADER) +end + +mod.describe = function(desc, func) + results.pass = results.pass or {} + results.fail = results.fail or {} + results.errs = results.errs or {} + + describe = mod.inner_describe + local ok, msg, desc_stack = call_inner(desc, func) + describe = mod.describe + + if not ok then + table.insert(results.errs, { + descriptions = desc_stack, + msg = msg, + }) + end +end + +mod.inner_describe = function(desc, func) + local ok, msg, desc_stack = call_inner(desc, func) + + if not ok then + table.insert(results.errs, { + descriptions = desc_stack, + msg = msg, + }) + end +end + +mod.before_each = function(fn) + table.insert(current_before_each[#current_description], fn) +end + +mod.after_each = function(fn) + table.insert(current_after_each[#current_description], fn) +end + +mod.clear = function() + vim.api.nvim_buf_set_lines(0, 0, -1, false, {}) +end + +local indent = function(msg, spaces) + if spaces == nil then + spaces = 4 + end + + local prefix = string.rep(" ", spaces) + return prefix .. msg:gsub("\n", "\n" .. prefix) +end + +local run_each = function(tbl) + for _, v in ipairs(tbl) do + for _, w in ipairs(v) do + if type(w) == "function" then + w() + end + end + end +end + +mod.it = function(desc, func) + run_each(current_before_each) + local ok, msg, desc_stack = call_inner(desc, func) + run_each(current_after_each) + + local test_result = { + descriptions = desc_stack, + msg = nil, + } + + -- TODO: We should figure out how to determine whether + -- and assert failed or whether it was an error... + + local to_insert + if not ok then + to_insert = results.fail + test_result.msg = msg + + print(FAIL, "||", table.concat(test_result.descriptions, " ")) + print(indent(msg, 12)) + else + to_insert = results.pass + print(SUCCESS, "||", table.concat(test_result.descriptions, " ")) + end + + table.insert(to_insert, test_result) +end + +mod.pending = function(desc, func) + local curr_stack = vim.deepcopy(current_description) + table.insert(curr_stack, desc) + print(PENDING, "||", table.concat(curr_stack, " ")) +end + +_PlenaryBustedOldAssert = _PlenaryBustedOldAssert or assert + +describe = mod.describe +it = mod.it +pending = mod.pending +before_each = mod.before_each +after_each = mod.after_each +clear = mod.clear +---@type Luassert +assert = require "luassert" + +mod.run = function(file) + file = file:gsub("\\", "/") + + print("\n" .. HEADER) + print("Testing: ", file) + + local loaded, msg = loadfile(file) + + if not loaded then + print(HEADER) + print "FAILED TO LOAD FILE" + print(color_string("red", msg)) + print(HEADER) + if is_headless then + return vim.cmd "2cq" + else + return + end + end + + coroutine.wrap(function() + loaded() + + -- If nothing runs (empty file without top level describe) + if not results.pass then + if is_headless then + return vim.cmd "0cq" + else + return + end + end + + mod.format_results(results) + + if #results.errs ~= 0 then + print("We had an unexpected error: ", vim.inspect(results.errs), vim.inspect(results)) + if is_headless then + return vim.cmd "2cq" + end + elseif #results.fail > 0 then + print "Tests Failed. Exit: 1" + + if is_headless then + return vim.cmd "1cq" + end + else + if is_headless then + return vim.cmd "0cq" + end + end + end)() +end + +return mod diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/class.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/class.lua new file mode 100644 index 0000000..e82f073 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/class.lua @@ -0,0 +1,80 @@ +---@brief [[ +---classic +--- +---Copyright (c) 2014, rxi +---@brief ]] + +---@class Object +local Object = {} +Object.__index = Object + +---Does nothing. +---You have to implement this yourself for extra functionality when initializing +---@param self Object +function Object:new() end + +---Create a new class/object by extending the base Object class. +---The extended object will have a field called `super` that will access the super class. +---@param self Object +---@return Object +function Object:extend() + local cls = {} + for k, v in pairs(self) do + if k:find "__" == 1 then + cls[k] = v + end + end + cls.__index = cls + cls.super = self + setmetatable(cls, self) + return cls +end + +---Implement a mixin onto this Object. +---@param self Object +---@param nil ... +function Object:implement(...) + for _, cls in pairs { ... } do + for k, v in pairs(cls) do + if self[k] == nil and type(v) == "function" then + self[k] = v + end + end + end +end + +---Checks if the object is an instance +---This will start with the lowest class and loop over all the superclasses. +---@param self Object +---@param T Object +---@return boolean +function Object:is(T) + local mt = getmetatable(self) + while mt do + if mt == T then + return true + end + mt = getmetatable(mt) + end + return false +end + +---The default tostring implementation for an object. +---You can override this to provide a different tostring. +---@param self Object +---@return string +function Object:__tostring() + return "Object" +end + +---You can call the class the initialize it without using `Object:new`. +---@param self Object +---@param nil ... +---@return Object +function Object:__call(...) + local obj = setmetatable({}, self) + obj:new(...) + return obj +end + +return Object diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/collections/py_list.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/collections/py_list.lua new file mode 100644 index 0000000..25df4b0 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/collections/py_list.lua @@ -0,0 +1,393 @@ +---@brief [[ +--- This module implements python-like lists. It can be used like so: +---
+---     local List = require 'plenary.collections.py_list'
+---     local l = List{3, 20, 44}
+---     print(l)  -- [3, 20, 44]
+--- 
+---@brief ]] +local List = {} + +---@class List @The base class for all list objects + +---List constructor. Can be used in higher order functions +---@param tbl table: A list-like table containing the initial elements of the list +---@return List: A new list object +function List.new(tbl) + if type(tbl) == "table" then + local len = #tbl + local obj = setmetatable(tbl, List) + obj._len = len + return obj + end + error "List constructor must be called with table argument" +end + +--- Checks whether the argument is a List object +--- @param tbl table: The object to test +--- @return boolean: Whether tbl is an instance of List +function List.is_list(tbl) + local meta = getmetatable(tbl) or {} + return meta == List +end + +function List:__index(key) + if self ~= List then + local field = List[key] + if field then + return field + end + end +end + +-- TODO: Similar to python, use [...] if the table references itself -- +function List:__tostring() + local elements = self:join ", " + return "[" .. elements .. "]" +end + +function List:__eq(other) + if #self ~= #other then + return false + end + for i = 1, #self do + if self[i] ~= other[i] then + return false + end + end + return true +end + +function List:__mul(other) + local result = List.new {} + for i = 1, other do + result[i] = self + end + return result +end + +function List:__len() + return self._len +end + +function List:__concat(other) + return self:concat(other) +end + +--- Pushes the element to the end of the list +--- @param other any: The object to append +--- @see List.pop +function List:push(other) + self[#self + 1] = other + self._len = self._len + 1 +end + +--- Pops the last element off the list and returns it +--- @return any: The (previously) last element from the list +--- @see List.push +function List:pop() + local result = table.remove(self) + self._len = self._len - 1 + return result +end + +--- Inserts other into the specified idx +--- @param idx number: The index that other will be inserted to +--- @param other any: The element to insert +--- @see List.remove +function List:insert(idx, other) + table.insert(self, idx, other) + self._len = self._len + 1 +end + +--- Removes the element at index idx and returns it +--- @param idx number: The index of the element to remove +--- @return any: The element previously at index idx +--- @see List.insert +function List:remove(idx) + self._len = self._len - 1 + return table.remove(self, idx) +end + +--- Can be used to compare elements with any list-like table. It only checks for +--- shallow equality +--- @param other any: The element to test for +--- @return boolean: True if other is a list object and all it's elements are equal +--- @see List.deep_equal +function List:equal(other) + return self:__eq(other) +end + +--- Checks for deep equality between lists. This uses vim.deep_equal for testing +--- @param other any: The element to test for +--- @return boolean: True if all elements and their children are equal +--- @see List.equal +--- @see vim.deep_equal +function List:deep_equal(other) + return vim.deep_equal(self, other) +end + +--- Returns a copy of the list with elements between a and b, inclusive +---
+---     local list = List{1, 2, 3, 4}
+---     local slice = list:slice(2, 3)
+---     print(slice) -- [2, 3]
+--- 
+--- @param a number: The low end of the slice +--- @param b number: The high end of the slice +--- @return List: A list with elements between a and b +function List:slice(a, b) + return List.new(vim.list_slice(self, a, b)) +end + +--- Similar to slice, but with every element. It only makes a shallow copy +--- @return List: A slice from 1 to #self, i.e., a complete copy of the list +--- @see List.deep_copy +function List:copy() + return self:slice(1, #self) +end + +--- Similar to copy, but makes a deep copy instead +--- @return List: A deep copy of the object +--- @see List.copy +--- @see vim.deep_copy +function List:deep_copy() + return vim.deep_copy(self) +end + +--- Reverses the list in place. If you don't want this, you could do something +--- like this +---
+---     local list = List{1, 2, 3, 4}
+---     local reversed = list:copy():reverse()
+--- 
+--- @return List: The list itself, so you can chain method calls +--- @see List.copy +--- @see List.deep_copy +function List:reverse() + local n = #self + local i = 1 + while i < n do + self[i], self[n] = self[n], self[i] + i = i + 1 + n = n - 1 + end + return self +end + +--- Concatenates the elements whithin the list separated by the given string +---
+---     local list = List{1, 2, 3, 4}
+---     print(list:join('-'))  -- 1-2-3-4
+--- 
+--- @param sep string: The separator to place between the elements. Default '' +--- @return string: The elements in the list separated by sep +function List:join(sep) + sep = sep or "" + local result = "" + for i, v in self:iter() do + result = result .. tostring(v) + if i ~= #self then + result = result .. sep + end + end + return result +end + +--- Returns a list with the elements of self concatenated with those in the +--- given arguments +--- @vararg table|List: The sequences to concatenate to this one +--- @return List +function List:concat(...) + local result = self:copy() + local others = { ... } + for _, other in ipairs(others) do + for _, v in ipairs(other) do + result:push(v) + end + end + return result +end + +--- Moves the elements between from and from+len in self, to positions between +--- to and to+len in other, like so +---
+---     other[to], other[to+1]... other[to+len] = self[from], self[from+1]... self[from+len]
+--- 
+--- @param from number: The first index of the origin slice +--- @param len number: The length of the slices +--- @param to number: The first index of the destination slice +--- @param other table|List: The destination list. Defaults to self +--- @see table.move +function List:move(from, len, to, other) + return table.move(self, from, len, to, other) +end + +--- Packs the given elements into a list. Similar to lua 5.3's table.pack +--- @vararg any: The elements to pack +--- @return List: a list containing all the given elements +--- @see table.pack +function List.pack(...) + return List.new { ... } +end + +--- Unpacks the elements from this list and returns them +--- @return ...any: All the elements from self[1] to self[#self] +function List:unpack() + return unpack(self, 1, #self) +end + +-- Iterator stuff + +local Iter = require "plenary.iterators" + +local itermetatable = getmetatable(Iter:wrap()) + +local function forward_list_gen(param, state) + state = state + 1 + local v = param[state] + if v ~= nil then + return state, v + end +end + +local function backward_list_gen(param, state) + state = state - 1 + local v = param[state] + if v ~= nil then + return state, v + end +end + +--- Run the given predicate through all the elements pointed by this iterator, +--- and classify them into two lists. The first one holds the elements for which +--- predicate returned a truthy value, and the second holds the rest. For +--- example: +--- +---
+---     local list = List{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
+---     local evens, odds = list:iter():partition(function(e)
+---         return e % 2 == 0
+---     end)
+---     print(evens, odds)
+--- 
+--- +--- Would print +--- +---
+---     [0, 2, 4, 6, 8] [1, 3, 5, 7, 9]
+--- 
+---@param predicate function: The predicate to classify the elements +---@return List,List +local function partition(self, predicate) + local list1, list2 = List.new {}, List.new {} + for _, v in self do + if predicate(v) then + list1:push(v) + else + list2:push(v) + end + end + return list1, list2 +end + +local function wrap_iter(f, l, n) + local iter = Iter.wrap(f, l, n) + iter.partition = partition + return iter +end + +--- Counts the occurrences of e inside the list +--- @param e any: The element to test for +--- @return number: The number of occurrences of e +function List:count(e) + local count = 0 + for _, v in self:iter() do + if e == v then + count = count + 1 + end + end + return count +end + +--- Appends the elements in the given iterator to the list +--- @param other table: An iterator object +function List:extend(other) + if type(other) == "table" and getmetatable(other) == itermetatable then + for _, v in other do + self:push(v) + end + else + error "Argument must be an iterator" + end +end + +--- Checks whether there is an occurence of the given element in the list +--- @param e any: The object to test for +--- @return boolean: True if e is present +function List:contains(e) + for _, v in self:iter() do + if v == e then + return true + end + end + return false +end + +--- Creates an iterator for the list. For example: +---
+---     local list = List{8, 4, 7, 9}
+---     for i, v in list:iter() do
+---         print(i, v)
+---     end
+--- 
+--- Would print: +---
+---     1    8
+---     2    4
+---     3    7
+---     4    9
+--- 
+--- @return table: An iterator object +function List:iter() + return wrap_iter(forward_list_gen, self, 0) +end + +--- Creates a reverse iterator for the list. For example: +---
+---     local list = List{8, 4, 7, 9}
+---     for i, v in list:riter() do
+---         print(i, v)
+---     end
+--- 
+--- Would print: +---
+---     4    9
+---     3    7
+---     2    4
+---     1    8
+--- 
+--- @return table: An iterator object +function List:riter() + return wrap_iter(backward_list_gen, self, #self + 1) +end + +-- Miscellaneous + +--- Create a list from the elements pointed at by the given iterator. +--- @param iter table: An iterator object +--- @return List +function List.from_iter(iter) + local result = List.new {} + for _, v in iter do + result:push(v) + end + return result +end + +return setmetatable({}, { + __call = function(_, tbl) + return List.new(tbl) + end, + __index = List, +}) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/compat.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/compat.lua new file mode 100644 index 0000000..5e7e24d --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/compat.lua @@ -0,0 +1,17 @@ +local m = {} + +m.flatten = (function() + if vim.fn.has "nvim-0.11" == 1 then + return function(t) + return vim.iter(t):flatten():totable() + end + else + return function(t) + return vim.tbl_flatten(t) + end + end +end)() + +m.islist = vim.islist or vim.tbl_islist + +return m diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/context_manager.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/context_manager.lua new file mode 100644 index 0000000..7d617da --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/context_manager.lua @@ -0,0 +1,56 @@ +--- I like context managers for Python +--- I want them in Lua. + +local context_manager = {} + +function context_manager.with(obj, callable) + -- Wrap functions for people since we're nice + if type(obj) == "function" then + obj = coroutine.create(obj) + end + + if type(obj) == "thread" then + local ok, context = coroutine.resume(obj) + assert(ok, "Should have yielded in coroutine.") + + local succeeded, result = pcall(callable, context) + + local done, _ = coroutine.resume(obj) + assert(done, "Should be done") + + local no_other = not coroutine.resume(obj) + assert(no_other, "Should not yield anymore, otherwise that would make things complicated") + + assert(succeeded, result) + return result + else + assert(obj.enter) + assert(obj.exit) + + -- TODO: Callable can be string for vimL function or a lua callable + local context = obj:enter() + local succeeded, result = pcall(callable, context) + obj:exit() + + assert(succeeded, result) + return result + end +end + +--- @param filename string|table -- If string, used as io.open(filename) +--- Else, should be a table with `filename` as an attribute +function context_manager.open(filename, mode) + if type(filename) == "table" and filename.filename then + filename = filename.filename + end + + local file_io = assert(io.open(filename, mode)) + + return coroutine.create(function() + coroutine.yield(file_io) + + file_io:close() + end) +end + +return context_manager diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/curl.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/curl.lua new file mode 100644 index 0000000..321bed8 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/curl.lua @@ -0,0 +1,359 @@ +--[[ +Curl Wrapper + +all curl methods accepts + + url = "The url to make the request to.", (string) + query = "url query, append after the url", (table) + body = "The request body" (string/filepath/table) + auth = "Basic request auth, 'user:pass', or {"user", "pass"}" (string/array) + form = "request form" (table) + raw = "any additonal curl args, it must be an array/list." (array) + dry_run = "whether to return the args to be ran through curl." (boolean) + output = "where to download something." (filepath) + timeout = "request timeout in mseconds" (number) + http_version = "HTTP version to use: 'HTTP/0.9', 'HTTP/1.0', 'HTTP/1.1', 'HTTP/2', or 'HTTP/3'" (string) + proxy = "[protocol://]host[:port] Use this proxy" (string) + insecure = "Allow insecure server connections" (boolean) + +and returns table: + + exit = "The shell process exit code." (number) + status = "The https response status." (number) + headers = "The https response headers." (array) + body = "The http response body." (string) + +see test/plenary/curl_spec.lua for examples. + +author = github.com/tami5 +]] +-- + +local util, parse = {}, {} + +-- Helpers -------------------------------------------------- +------------------------------------------------------------- +local F = require "plenary.functional" +local J = require "plenary.job" +local P = require "plenary.path" +local compat = require "plenary.compat" + +-- Utils ---------------------------------------------------- +------------------------------------------------------------- + +util.url_encode = function(str) + if type(str) ~= "number" then + str = str:gsub("\r?\n", "\r\n") + str = str:gsub("([^%w%-%.%_%~ ])", function(c) + return string.format("%%%02X", c:byte()) + end) + str = str:gsub(" ", "+") + return str + else + return str + end +end + +util.kv_to_list = function(kv, prefix, sep) + return compat.flatten(F.kv_map(function(kvp) + return { prefix, kvp[1] .. sep .. kvp[2] } + end, kv)) +end + +util.kv_to_str = function(kv, sep, kvsep) + return F.join( + F.kv_map(function(kvp) + return kvp[1] .. kvsep .. util.url_encode(kvp[2]) + end, kv), + sep + ) +end + +util.gen_dump_path = function() + local path + local id = string.gsub("xxxx4xxx", "[xy]", function(l) + local v = (l == "x") and math.random(0, 0xf) or math.random(0, 0xb) + return string.format("%x", v) + end) + if P.path.sep == "\\" then + path = string.format("%s\\AppData\\Local\\Temp\\plenary_curl_%s.headers", os.getenv "USERPROFILE", id) + else + local temp_dir = os.getenv "XDG_RUNTIME_DIR" or "/tmp" + path = temp_dir .. "/plenary_curl_" .. id .. ".headers" + end + return { "-D", path } +end + +-- Parsers ---------------------------------------------------- +--------------------------------------------------------------- + +parse.headers = function(t) + if not t then + return + end + local upper = function(str) + return string.gsub(" " .. str, "%W%l", string.upper):sub(2) + end + return util.kv_to_list( + (function() + local normilzed = {} + for k, v in pairs(t) do + normilzed[upper(k:gsub("_", "%-"))] = v + end + return normilzed + end)(), + "-H", + ": " + ) +end + +parse.data_body = function(t) + if not t then + return + end + return util.kv_to_list(t, "-d", "=") +end + +parse.raw_body = function(xs) + if not xs then + return + end + if type(xs) == "table" then + return parse.data_body(xs) + else + return { "--data-raw", xs } + end +end + +parse.form = function(t) + if not t then + return + end + return util.kv_to_list(t, "-F", "=") +end + +parse.curl_query = function(t) + if not t then + return + end + return util.kv_to_str(t, "&", "=") +end + +parse.method = function(s) + if not s then + return + end + if s ~= "head" then + return { "-X", string.upper(s) } + else + return { "-I" } + end +end + +parse.file = function(p) + if not p then + return + end + return { "-d", "@" .. P.expand(P.new(p)) } +end + +parse.auth = function(xs) + if not xs then + return + end + return { "-u", type(xs) == "table" and util.kv_to_str(xs, nil, ":") or xs } +end + +parse.url = function(xs, q) + if not xs then + return + end + q = parse.curl_query(q) + if type(xs) == "string" then + return q and xs .. "?" .. q or xs + elseif type(xs) == "table" then + error "Low level URL definition is not supported." + end +end + +parse.accept_header = function(s) + if not s then + return + end + return { "-H", "Accept: " .. s } +end + +parse.http_version = function(s) + if not s then + return + end + if s == "HTTP/0.9" or s == "HTTP/1.0" or s == "HTTP/1.1" or s == "HTTP/2" or s == "HTTP/3" then + s = s:lower() + s = s:gsub("/", "") + return { "--" .. s } + else + error "Unknown HTTP version." + end +end + +-- Parse Request ------------------------------------------- +------------------------------------------------------------ +parse.request = function(opts) + if opts.body then + local b = opts.body + local silent_is_file = function() + local status, result = pcall(P.is_file, P.new(b)) + return status and result + end + opts.body = nil + if type(b) == "table" then + opts.data = b + elseif silent_is_file() then + opts.in_file = b + elseif type(b) == "string" then + opts.raw_body = b + end + end + local result = { "-sSL", opts.dump } + local append = function(v) + if v then + table.insert(result, v) + end + end + + if opts.insecure then + table.insert(result, "--insecure") + end + if opts.proxy then + table.insert(result, { "--proxy", opts.proxy }) + end + if opts.compressed then + table.insert(result, "--compressed") + end + append(parse.method(opts.method)) + append(parse.headers(opts.headers)) + append(parse.accept_header(opts.accept)) + append(parse.raw_body(opts.raw_body)) + append(parse.data_body(opts.data)) + append(parse.form(opts.form)) + append(parse.file(opts.in_file)) + append(parse.auth(opts.auth)) + append(parse.http_version(opts.http_version)) + append(opts.raw) + if opts.output then + table.insert(result, { "-o", opts.output }) + end + table.insert(result, parse.url(opts.url, opts.query)) + return compat.flatten(result), opts +end + +-- Parse response ------------------------------------------ +------------------------------------------------------------ +parse.response = function(lines, dump_path, code) + local headers = P.readlines(dump_path) + local status = nil + local processed_headers = {} + + -- Process headers in a single pass + for _, line in ipairs(headers) do + local status_match = line:match "^HTTP/%S*%s+(%d+)" + if status_match then + status = tonumber(status_match) + elseif line ~= "" then + table.insert(processed_headers, line) + end + end + + local body = F.join(lines, "\n") + vim.loop.fs_unlink(dump_path) + + return { + status = status or 0, + headers = processed_headers, + body = body, + exit = code, + } +end + +local request = function(specs) + local response = {} + local args, opts = parse.request(vim.tbl_extend("force", { + compressed = package.config:sub(1, 1) ~= "\\", + dry_run = false, + dump = util.gen_dump_path(), + }, specs)) + + if opts.dry_run then + return args + end + + local job_opts = { + command = vim.g.plenary_curl_bin_path or "curl", + args = args, + } + + if opts.stream then + job_opts.on_stdout = opts.stream + end + + job_opts.on_exit = function(j, code) + if code ~= 0 then + local stderr = vim.inspect(j:stderr_result()) + local message = string.format("%s %s - curl error exit_code=%s stderr=%s", opts.method, opts.url, code, stderr) + if opts.on_error then + return opts.on_error { + message = message, + stderr = stderr, + exit = code, + } + else + error(message) + end + end + local output = parse.response(j:result(), opts.dump[2], code) + if opts.callback then + return opts.callback(output) + else + response = output + end + end + local job = J:new(job_opts) + + if opts.callback or opts.stream then + job:start() + return job + else + local timeout = opts.timeout or 10000 + job:sync(timeout) + return response + end +end + +-- Main ---------------------------------------------------- +------------------------------------------------------------ +return (function() + local partial = function(method) + return function(url, opts) + local spec = {} + opts = opts or {} + if type(url) == "table" then + opts = url + spec.method = method + else + spec.url = url + spec.method = method + end + opts = method == "request" and opts or (vim.tbl_extend("keep", opts, spec)) + return request(opts) + end + end + return { + get = partial "get", + post = partial "post", + put = partial "put", + head = partial "head", + patch = partial "patch", + delete = partial "delete", + request = partial "request", + } +end)() diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/debug_utils.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/debug_utils.lua new file mode 100644 index 0000000..c4caf65 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/debug_utils.lua @@ -0,0 +1,13 @@ +local debug_utils = {} + +function debug_utils.sourced_filepath() + local str = debug.getinfo(2, "S").source:sub(2) + return str +end + +function debug_utils.sourced_filename() + local str = debug_utils.sourced_filepath() + return str:match "^.*/(.*).lua$" or str +end + +return debug_utils diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/enum.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/enum.lua new file mode 100644 index 0000000..22d4410 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/enum.lua @@ -0,0 +1,162 @@ +---@brief [[ +--- This module defines an idiomatic way to create enum classes, similar to +--- those in java or kotlin. There are two ways to create an enum, one is with +--- the exported `make_enum` function, or calling the module directly with the +--- enum spec. +--- +--- The enum spec consists of a list-like table whose members can be either a +--- string or a tuple of the form {string, number}. In the former case, the enum +--- member will take the next available value, while in the latter, the member +--- will take the string as it's name and the number as it's value. In both +--- cases, the name must start with a capital letter. +--- +--- Here is an example: +--- +---
+--- local Enum = require 'plenary.enum'
+--- local myEnum = Enum {
+---     'Foo',          -- Takes value 1
+---     'Bar',          -- Takes value 2
+---     {'Qux', 10},    -- Takes value 10
+---     'Baz',          -- Takes value 11
+--- }
+--- 
+--- +--- In case of name or value clashing, the call will fail. For this reason, it's +--- best if you define the members in ascending order. +---@brief ]] +local Enum = {} + +---@class Enum + +---@class Variant + +local function validate_member_name(name) + if #name > 0 and name:sub(1, 1):match "%u" then + return name + end + error('"' .. name .. '" should start with a capital letter') +end + +--- Creates an enum from the given list-like table, like so: +---
+--- local enum = Enum.make_enum{
+---     'Foo',
+---     'Bar',
+---     {'Qux', 10}
+--- }
+--- 
+--- @return Enum: A new enum +local function make_enum(tbl) + local enum = {} + + local Variant = {} + Variant.__index = Variant + + local function newVariant(i) + return setmetatable({ value = i }, Variant) + end + + -- we don't need __eq because the __eq metamethod will only ever be + -- invoked when they both have the same metatable + + function Variant:__lt(o) + return self.value < o.value + end + + function Variant:__gt(o) + return self.value > o.value + end + + function Variant:__tostring() + return tostring(self.value) + end + + local function find_next_idx(e, i) + local newI = i + 1 + if not e[newI] then + return newI + end + error("Overlapping index: " .. tostring(newI)) + end + + local i = 0 + + for _, v in ipairs(tbl) do + if type(v) == "string" then + local name = validate_member_name(v) + local idx = find_next_idx(enum, i) + enum[idx] = name + if enum[name] then + error("Duplicate enum member name: " .. name) + end + enum[name] = newVariant(idx) + i = idx + elseif type(v) == "table" and type(v[1]) == "string" and type(v[2]) == "number" then + local name = validate_member_name(v[1]) + local idx = v[2] + if enum[idx] then + error("Overlapping index: " .. tostring(idx)) + end + enum[idx] = name + if enum[name] then + error("Duplicate name: " .. name) + end + enum[name] = newVariant(idx) + i = idx + else + error "Invalid way to specify an enum variant" + end + end + + return require("plenary.tbl").freeze(setmetatable(enum, Enum)) +end + +Enum.__index = function(_, key) + if Enum[key] then + return Enum[key] + end + error("Invalid enum key: " .. tostring(key)) +end + +--- Checks whether the enum has a member with the given name +--- @param key string: The element to check for +--- @return boolean: True if key is present +function Enum:has_key(key) + if rawget(getmetatable(self).__index, key) then + return true + end + return false +end + +--- If there is a member named 'key', return it, otherwise return nil +--- @param key string: The element to check for +--- @return Variant: The element named by key, or nil if not present +function Enum:from_str(key) + if self:has_key(key) then + return self[key] + end +end + +--- If there is a member of value 'num', return it, otherwise return nil +--- @param num number: The value of the element to check for +--- @return Variant: The element whose value is num +function Enum:from_num(num) + local key = self[num] + if key then + return self[key] + end +end + +--- Checks whether the given object corresponds to an instance of Enum +--- @param tbl table: The object to be checked +--- @return boolean: True if tbl is an Enum +local function is_enum(tbl) + return getmetatable(getmetatable(tbl).__index) == Enum +end + +return setmetatable({ is_enum = is_enum, make_enum = make_enum }, { + __call = function(_, tbl) + return make_enum(tbl) + end, +}) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/errors.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/errors.lua new file mode 100644 index 0000000..64849f6 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/errors.lua @@ -0,0 +1,15 @@ +local M = {} + +M.traceback_error = function(s, level) + local traceback = debug.traceback() + traceback = traceback .. "\n" .. s + error(traceback, (level or 1) + 1) +end + +M.info_error = function(s, func_info, level) + local info = debug.getinfo(func_info) + info = info .. "\n" .. s + error(info, (level or 1) + 1) +end + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/filetype.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/filetype.lua new file mode 100644 index 0000000..60cd307 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/filetype.lua @@ -0,0 +1,196 @@ +local Path = require "plenary.path" + +local os_sep = Path.path.sep + +local filetype = {} + +local filetype_table = { + extension = {}, + file_name = {}, + shebang = {}, +} + +filetype.add_table = function(new_filetypes) + local valid_keys = { "extension", "file_name", "shebang" } + local new_keys = {} + + -- Validate keys + for k, _ in pairs(new_filetypes) do + new_keys[k] = true + end + for _, k in ipairs(valid_keys) do + new_keys[k] = nil + end + + for k, v in pairs(new_keys) do + error(debug.traceback("Invalid key / value:" .. tostring(k) .. " / " .. tostring(v))) + end + + if new_filetypes.extension then + filetype_table.extension = vim.tbl_extend("force", filetype_table.extension, new_filetypes.extension) + end + + if new_filetypes.file_name then + filetype_table.file_name = vim.tbl_extend("force", filetype_table.file_name, new_filetypes.file_name) + end + + if new_filetypes.shebang then + filetype_table.shebang = vim.tbl_extend("force", filetype_table.shebang, new_filetypes.shebang) + end +end + +filetype.add_file = function(filename) + local filetype_files = vim.api.nvim_get_runtime_file(string.format("data/plenary/filetypes/%s.lua", filename), true) + + for _, file in ipairs(filetype_files) do + local ok, msg = pcall(filetype.add_table, dofile(file)) + if not ok then + error("Unable to add file " .. file .. ":\n" .. msg) + end + end +end + +local filename_regex = "[^" .. os_sep .. "].*" +filetype._get_extension_parts = function(filename) + local current_match = filename:match(filename_regex) + local possibilities = {} + while current_match do + current_match = current_match:match "[^.]%.(.*)" + if current_match then + table.insert(possibilities, current_match:lower()) + else + return possibilities + end + end + return possibilities +end + +filetype._parse_modeline = function(tail) + if tail:find "vim:" then + return tail:match ".*:ft=([^: ]*):.*$" or "" + end + return "" +end + +filetype._parse_shebang = function(head) + if head:sub(1, 2) == "#!" then + local match = filetype_table.shebang[head:sub(3, #head)] + if match then + return match + end + end + return "" +end + +local done_adding = false +local extend_tbl_with_ext_eq_ft_entries = function() + if not done_adding then + if vim.in_fast_event() then + return + end + local all_valid_filetypes = vim.fn.getcompletion("", "filetype") + for _, v in ipairs(all_valid_filetypes) do + if not filetype_table.extension[v] then + filetype_table.extension[v] = v + end + end + done_adding = true + return true + end +end + +filetype.detect_from_extension = function(filepath) + local exts = filetype._get_extension_parts(filepath) + for _, ext in ipairs(exts) do + local match = ext and filetype_table.extension[ext] + if match then + return match + end + end + if extend_tbl_with_ext_eq_ft_entries() then + for _, ext in ipairs(exts) do + local match = ext and filetype_table.extension[ext] + if match then + return match + end + end + end + return "" +end + +filetype.detect_from_name = function(filepath) + if filepath then + filepath = filepath:lower() + local split_path = vim.split(filepath, os_sep, true) + local fname = split_path[#split_path] + local match = filetype_table.file_name[fname] + if match then + return match + end + end + return "" +end + +filetype.detect_from_modeline = function(filepath) + local tail = Path:new(filepath):readbyterange(-256, 256) + if not tail then + return "" + end + local lines = vim.split(tail, "\n") + local idx = lines[#lines] ~= "" and #lines or #lines - 1 + if idx >= 1 then + return filetype._parse_modeline(lines[idx]) + end +end + +filetype.detect_from_shebang = function(filepath) + local head = Path:new(filepath):readbyterange(0, 256) + if not head then + return "" + end + local lines = vim.split(head, "\n") + return filetype._parse_shebang(lines[1]) +end + +--- Detect a filetype from a path. +--- +---@param opts table: Table with optional keys +--- - fs_access (bool, default=true): Should check a file if it exists +filetype.detect = function(filepath, opts) + opts = opts or {} + opts.fs_access = opts.fs_access or true + + if type(filepath) ~= string then + filepath = tostring(filepath) + end + + local match = filetype.detect_from_name(filepath) + if match ~= "" then + return match + end + + match = filetype.detect_from_extension(filepath) + + if opts.fs_access and Path:new(filepath):exists() then + if match == "" then + match = filetype.detect_from_shebang(filepath) + if match ~= "" then + return match + end + end + + if match == "text" or match == "" then + match = filetype.detect_from_modeline(filepath) + if match ~= "" then + return match + end + end + end + + return match +end + +filetype.add_file "base" +filetype.add_file "builtin" + +return filetype diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/fun.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/fun.lua new file mode 100644 index 0000000..e70367f --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/fun.lua @@ -0,0 +1,23 @@ +local M = {} + +M.bind = require("plenary.functional").partial + +function M.arify(fn, argc) + return function(...) + if select("#", ...) ~= argc then + error(("Expected %s number of arguments"):format(argc)) + end + + fn(...) + end +end + +function M.create_wrapper(map) + return function(to_wrap) + return function(...) + return map(to_wrap(...)) + end + end +end + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/functional.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/functional.lua new file mode 100644 index 0000000..6a5b367 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/functional.lua @@ -0,0 +1,77 @@ +local f = {} + +function f.kv_pairs(t) + local results = {} + for k, v in pairs(t) do + table.insert(results, { k, v }) + end + return results +end + +function f.kv_map(fun, t) + return vim.tbl_map(fun, f.kv_pairs(t)) +end + +function f.join(array, sep) + return table.concat(vim.tbl_map(tostring, array), sep) +end + +local function bind_n(fn, n, a, ...) + if n == 0 then + return fn + end + return bind_n(function(...) + return fn(a, ...) + end, n - 1, ...) +end + +function f.partial(fun, ...) + return bind_n(fun, select("#", ...), ...) +end + +function f.any(fun, iterable) + for k, v in pairs(iterable) do + if fun(k, v) then + return true + end + end + + return false +end + +function f.all(fun, iterable) + for k, v in pairs(iterable) do + if not fun(k, v) then + return false + end + end + + return true +end + +function f.if_nil(val, was_nil, was_not_nil) + if val == nil then + return was_nil + else + return was_not_nil + end +end + +function f.select_only(n) + return function(...) + local x = select(n, ...) + return x + end +end + +f.first = f.select_only(1) +f.second = f.select_only(2) +f.third = f.select_only(3) + +function f.last(...) + local length = select("#", ...) + local x = select(length, ...) + return x +end + +return f diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/init.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/init.lua new file mode 100644 index 0000000..4805a4f --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/init.lua @@ -0,0 +1,14 @@ +-- Lazy load everything into plenary. +local plenary = setmetatable({}, { + __index = function(t, k) + local ok, val = pcall(require, string.format("plenary.%s", k)) + + if ok then + rawset(t, k, val) + end + + return val + end, +}) + +return plenary diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/iterators.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/iterators.lua new file mode 100644 index 0000000..2aac0d2 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/iterators.lua @@ -0,0 +1,695 @@ +---@brief [[ +---An adaptation of luafun for neovim. +---This library will use neovim specific functions. +---Some documentation is the same as from luafun. +---Some extra functions are present that are not in luafun +---@brief ]] + +local co = coroutine +local f = require "plenary.functional" +local compat = require "plenary.compat" + +-------------------------------------------------------------------------------- +-- Tools +-------------------------------------------------------------------------------- + +local exports = {} + +---@class Iterator +---@field gen function +---@field param any +---@field state any +local Iterator = {} +Iterator.__index = Iterator + +---Makes a for loop work +---If not called without param or state, will just generate with the starting state +---This is useful because the original luafun will also return param and state in addition to the iterator as a multival +---This can cause problems because when using iterators as expressions the multivals can bleed +---For example i.iter { 1, 2, i.iter { 3, 4 } } will not work because the inner iterator returns a multival thus +---polluting the list with internal values. +---So instead we do not return param and state as multivals when doing wrap +---This causes the first loop iteration to call param and state with nil because we didn't return them as multivals +---We have to use or to check for nil and default to interal starting state and param +function Iterator:__call(param, state) + return self.gen(param or self.param, state or self.state) +end + +function Iterator:__tostring() + return "" +end + +-- A special hack for zip/chain to skip last two state, if a wrapped iterator +-- has been passed +local numargs = function(...) + local n = select("#", ...) + if n >= 3 then + -- Fix last argument + local it = select(n - 2, ...) + if + type(it) == "table" + and getmetatable(it) == Iterator + and it.param == select(n - 1, ...) + and it.state == select(n, ...) + then + return n - 2 + end + end + return n +end + +local return_if_not_empty = function(state_x, ...) + if state_x == nil then + return nil + end + return ... +end + +local call_if_not_empty = function(fun, state_x, ...) + if state_x == nil then + return nil + end + return state_x, fun(...) +end + +-------------------------------------------------------------------------------- +-- Basic Functions +-------------------------------------------------------------------------------- +local nil_gen = function(_param, _state) + return nil +end + +local pairs_gen = pairs {} + +local map_gen = function(map, key) + local value + key, value = pairs_gen(map, key) + return key, key, value +end + +local string_gen = function(param, state) + state = state + 1 + if state > #param then + return nil + end + local r = string.sub(param, state, state) + return state, r +end + +local rawiter = function(obj, param, state) + assert(obj ~= nil, "invalid iterator") + + if type(obj) == "table" then + local mt = getmetatable(obj) + + if mt ~= nil then + if mt == Iterator then + return obj.gen, obj.param, obj.state + end + end + + if compat.islist(obj) then + return ipairs(obj) + else + -- hash + return map_gen, obj, nil + end + elseif type(obj) == "function" then + return obj, param, state + elseif type(obj) == "string" then + if #obj == 0 then + return nil_gen, nil, nil + end + + return string_gen, obj, 0 + end + + error(string.format('object %s of type "%s" is not iterable', obj, type(obj))) +end + +---Wraps the iterator triplet into a table to allow metamethods and calling with method form +---Important! We do not return param and state as multivals like the original luafun +---See the __call metamethod for more information +---@param gen any +---@param param any +---@param state any +---@return Iterator +local function wrap(gen, param, state) + return setmetatable({ + gen = gen, + param = param, + state = state, + }, Iterator) +end + +---Unwrap an iterator metatable into the iterator triplet +---@param self Iterator +---@return any +---@return any +---@return any +local unwrap = function(self) + return self.gen, self.param, self.state +end + +---Create an iterator from an object +---@param obj any +---@param param any (optional) +---@param state any (optional) +---@return Iterator +local iter = function(obj, param, state) + return wrap(rawiter(obj, param, state)) +end + +exports.iter = iter +exports.wrap = wrap +exports.unwrap = unwrap + +function Iterator:for_each(fn) + local param, state = self.param, self.state + repeat + state = call_if_not_empty(fn, self.gen(param, state)) + until state == nil +end + +function Iterator:stateful() + return wrap( + co.wrap(function() + self:for_each(function(...) + co.yield(f.first(...), ...) + end) + + -- too make sure that we always return nil if there are no more + while true do + co.yield() + end + end), + nil, + nil + ) +end + +-- function Iterator:stateful() +-- local gen, param, state = self.gen, self.param, self.state + +-- local function return_and_set_state(state_x, ...) +-- state = state_x +-- if state == nil then return end +-- return state_x, ... +-- end + +-- local stateful_gen = function() +-- return return_and_set_state(gen(param, state)) +-- end + +-- return wrap(stateful_gen, false, false) +-- end + +-------------------------------------------------------------------------------- +-- Generators +-------------------------------------------------------------------------------- +local range_gen = function(param, state) + local stop, step = param[1], param[2] + state = state + step + if state > stop then + return nil + end + return state, state +end + +local range_rev_gen = function(param, state) + local stop, step = param[1], param[2] + state = state + step + if state < stop then + return nil + end + return state, state +end + +---Creates a range iterator +---@param start number +---@param stop number +---@param step number +---@return Iterator +local range = function(start, stop, step) + if step == nil then + if stop == nil then + if start == 0 then + return nil_gen, nil, nil + end + stop = start + start = stop > 0 and 1 or -1 + end + step = start <= stop and 1 or -1 + end + + assert(type(start) == "number", "start must be a number") + assert(type(stop) == "number", "stop must be a number") + assert(type(step) == "number", "step must be a number") + assert(step ~= 0, "step must not be zero") + + if step > 0 then + return wrap(range_gen, { stop, step }, start - step) + elseif step < 0 then + return wrap(range_rev_gen, { stop, step }, start - step) + end +end +exports.range = range + +local duplicate_table_gen = function(param_x, state_x) + return state_x + 1, unpack(param_x) +end + +local duplicate_fun_gen = function(param_x, state_x) + return state_x + 1, param_x(state_x) +end + +local duplicate_gen = function(param_x, state_x) + return state_x + 1, param_x +end + +---Creates an infinite iterator that will yield the arguments +---If multiple arguments are passed, the args will be packed and unpacked +---@param ...: the arguments to duplicate +---@return Iterator +local duplicate = function(...) + if select("#", ...) <= 1 then + return wrap(duplicate_gen, select(1, ...), 0) + else + return wrap(duplicate_table_gen, { ... }, 0) + end +end +exports.duplicate = duplicate + +---Creates an iterator from a function +---NOTE: if the function is a closure and modifies state, the resulting iterator will not be stateless +---@param fun function +---@return Iterator +local from_fun = function(fun) + assert(type(fun) == "function") + return wrap(duplicate_fun_gen, fun, 0) +end +exports.from_fun = from_fun + +---Creates an infinite iterator that will yield zeros. +---This is an alias to calling duplicate(0) +---@return Iterator +local zeros = function() + return wrap(duplicate_gen, 0, 0) +end +exports.zeros = zeros + +---Creates an infinite iterator that will yield ones. +---This is an alias to calling duplicate(1) +---@return Iterator +local ones = function() + return wrap(duplicate_gen, 1, 0) +end +exports.ones = ones + +local rands_gen = function(param_x, _state_x) + return 0, math.random(param_x[1], param_x[2]) +end + +local rands_nil_gen = function(_param_x, _state_x) + return 0, math.random() +end + +---Creates an infinite iterator that will yield random values. +---@param n number +---@param m number +---@return Iterator +local rands = function(n, m) + if n == nil and m == nil then + return wrap(rands_nil_gen, 0, 0) + end + assert(type(n) == "number", "invalid first arg to rands") + if m == nil then + m = n + n = 0 + else + assert(type(m) == "number", "invalid second arg to rands") + end + assert(n < m, "empty interval") + return wrap(rands_gen, { n, m - 1 }, 0) +end +exports.rands = rands + +local split_gen = function(param, state) + local input, sep = param[1], param[2] + local input_len = #input + + if state > input_len + 1 then + return + end + + local start, finish = string.find(input, sep, state, true) + if not start then + start = input_len + 1 + finish = input_len + 1 + end + + local sub_str = input:sub(state, start - 1) + + return finish + 1, sub_str +end + +---Return an iterator of substrings separated by a string +---@param input string: the string to split +---@param sep string: the separator to find and split based on +---@return Iterator +local split = function(input, sep) + return wrap(split_gen, { input, sep }, 1) +end +exports.split = split + +---Splits a string based on a single space +---An alias for split(input, " ") +---@param input any +---@return any +local words = function(input) + return split(input, " ") +end +exports.words = words + +local lines = function(input) + -- TODO: platform specific linebreaks + return split(input, "\n") +end +exports.lines = lines + +-------------------------------------------------------------------------------- +-- Transformations +-------------------------------------------------------------------------------- +local map_gen2 = function(param, state) + local gen_x, param_x, fun = param[1], param[2], param[3] + return call_if_not_empty(fun, gen_x(param_x, state)) +end + +---Iterator adapter that maps the previous iterator with a function +---@param fun function: The function to map with. Will be called on each element +---@return Iterator +function Iterator:map(fun) + return wrap(map_gen2, { self.gen, self.param, fun }, self.state) +end + +local flatten_gen1 +do + local it = function(new_iter, state_x, ...) + if state_x == nil then + return nil + end + return { new_iter.gen, new_iter.param, state_x }, ... + end + + flatten_gen1 = function(state, state_x, ...) + if state_x == nil then + return nil + end + + local first_arg = f.first(...) + + -- experimental part + if getmetatable(first_arg) == Iterator then + -- attach the iterator to the rest + local new_iter = (first_arg .. wrap(state[1], state[2], state_x)):flatten() + -- advance the iterator by one + return it(new_iter, new_iter.gen(new_iter.param, new_iter.state)) + end + + return { state[1], state[2], state_x }, ... + end +end + +local flatten_gen = function(_, state) + if state == nil then + return + end + local gen_x, param_x, state_x = state[1], state[2], state[3] + return flatten_gen1(state, gen_x(param_x, state_x)) +end + +---Iterator adapter that will recursivley flatten nested iterator structure +---@return Iterator +function Iterator:flatten() + return wrap(flatten_gen, false, { self.gen, self.param, self.state }) +end + +-------------------------------------------------------------------------------- +-- Filtering +-------------------------------------------------------------------------------- +local filter1_gen = function(fun, gen_x, param_x, state_x, a) + while true do + if state_x == nil or fun(a) then + break + end + state_x, a = gen_x(param_x, state_x) + end + return state_x, a +end + +-- call each other +-- because we can't assign a vararg mutably in a while loop like filter1_gen +-- so we have to use recursion in calling both of these functions +local filterm_gen +local filterm_gen_shrink = function(fun, gen_x, param_x, state_x) + return filterm_gen(fun, gen_x, param_x, gen_x(param_x, state_x)) +end + +filterm_gen = function(fun, gen_x, param_x, state_x, ...) + if state_x == nil then + return nil + end + + if fun(...) then + return state_x, ... + end + + return filterm_gen_shrink(fun, gen_x, param_x, state_x) +end + +local filter_detect = function(fun, gen_x, param_x, state_x, ...) + if select("#", ...) < 2 then + return filter1_gen(fun, gen_x, param_x, state_x, ...) + else + return filterm_gen(fun, gen_x, param_x, state_x, ...) + end +end + +local filter_gen = function(param, state_x) + local gen_x, param_x, fun = param[1], param[2], param[3] + return filter_detect(fun, gen_x, param_x, gen_x(param_x, state_x)) +end + +---Iterator adapter that will filter values +---@param fun function: The function to filter values with. If the function returns true, the value will be kept. +---@return Iterator +function Iterator:filter(fun) + return wrap(filter_gen, { self.gen, self.param, fun }, self.state) +end + +---Iterator adapter that will provide numbers from 1 to n as the first multival +---@return Iterator +function Iterator:enumerate() + local i = 0 + return self:map(function(...) + i = i + 1 + return i, ... + end) +end + +-------------------------------------------------------------------------------- +-- Reducing +-------------------------------------------------------------------------------- + +---Returns true if any of the values in the iterator satisfy a predicate +---@param fun function +---@return boolean +function Iterator:any(fun) + local r + local state, param, gen = self.state, self.param, self.gen + repeat + state, r = call_if_not_empty(fun, gen(param, state)) + until state == nil or r + return r +end + +---Returns true if all of the values in the iterator satisfy a predicate +---@param fun function +---@return boolean +function Iterator:all(fun) + local r + local state, param, gen = self.state, self.param, self.gen + repeat + state, r = call_if_not_empty(fun, gen(param, state)) + until state == nil or not r + return state == nil +end + +---Finds a value that is equal to the provided value of satisfies a predicate. +---@param val_or_fn any +---@return any +function Iterator:find(val_or_fn) + local gen, param, state = self.gen, self.param, self.state + if type(val_or_fn) == "function" then + return return_if_not_empty(filter_detect(val_or_fn, gen, param, gen(param, state))) + else + for _, r in gen, param, state do + if r == val_or_fn then + return r + end + end + return nil + end +end + +---Folds an iterator into a single value using a function. +---@param init any +---@param fun fun(acc: any, val: any): any +---@return any +function Iterator:fold(init, fun) + local acc = init + local gen, param, state = self.gen, self.param, self.state + for _, r in gen, param, state do + acc = fun(acc, r) + end + return acc +end + +---Turns an iterator into a list. +---If the iterator yields multivals only the first multival will be used. +---@return table +function Iterator:tolist() + local list = {} + self:for_each(function(a) + table.insert(list, a) + end) + return list +end + +---Turns an iterator into a list. +---If the iterator yields multivals all multivals will be used and packed into a table. +---@return table +function Iterator:tolistn() + local list = {} + self:for_each(function(...) + table.insert(list, { ... }) + end) + return list +end + +---Turns an iterator into a map. +---The first multival that the iterator yields will be the key. +---The second multival that the iterator yields will be the value. +---@return table +function Iterator:tomap() + local map = {} + self:for_each(function(key, value) + map[key] = value + end) + return map +end + +-------------------------------------------------------------------------------- +-- Compositions +-------------------------------------------------------------------------------- +-- call each other +local chain_gen_r1 +local chain_gen_r2 = function(param, state, state_x, ...) + if state_x == nil then + local i = state[1] + 1 + if param[3 * i - 1] == nil then + return nil + end + state_x = param[3 * i] + return chain_gen_r1(param, { i, state_x }) + end + return { state[1], state_x }, ... +end + +chain_gen_r1 = function(param, state) + local i, state_x = state[1], state[2] + local gen_x, param_x = param[3 * i - 2], param[3 * i - 1] + return chain_gen_r2(param, state, gen_x(param_x, state_x)) +end + +---Make an iterator that returns elements from the first iterator until it is exhausted, +---then proceeds to the next iterator, +---until all of the iterators are exhausted. +---Used for treating consecutive iterators as a single iterator. +---Infinity iterators are supported, but are not recommended. +---@param ...: the iterators to chain +---@return Iterator +local chain = function(...) + local n = numargs(...) + + if n == 0 then + return wrap(nil_gen, nil, nil) + end + + local param = { [3 * n] = 0 } + + local gen_x, param_x, state_x + for i = 1, n, 1 do + local elem = select(i, ...) + gen_x, param_x, state_x = unwrap(elem) + param[3 * i - 2] = gen_x + param[3 * i - 1] = param_x + param[3 * i] = state_x + end + + return wrap(chain_gen_r1, param, { 1, param[3] }) +end + +Iterator.chain = chain +Iterator.__concat = chain +exports.chain = chain + +local function zip_gen_r(param, state, state_new, ...) + if #state_new == #param / 2 then + return state_new, ... + end + + local i = #state_new + 1 + local gen_x, param_x = param[2 * i - 1], param[2 * i] + local state_x, r = gen_x(param_x, state[i]) + if state_x == nil then + return nil + end + table.insert(state_new, state_x) + return zip_gen_r(param, state, state_new, r, ...) +end + +local zip_gen = function(param, state) + return zip_gen_r(param, state, {}) +end + +---Return a new iterator where i-th return value contains the i-th element from each of the iterators. +---The returned iterator is truncated in length to the length of the shortest iterator. +---For multi-return iterators only the first variable is used. +---@param ...: the iterators to zip +---@return Iterator +local zip = function(...) + local n = numargs(...) + if n == 0 then + return wrap(nil_gen, nil, nil) + end + local param = { [2 * n] = 0 } + local state = { [n] = 0 } + + local gen_x, param_x, state_x + for i = 1, n, 1 do + local it = select(n - i + 1, ...) + gen_x, param_x, state_x = rawiter(it) + param[2 * i - 1] = gen_x + param[2 * i] = param_x + state[i] = state_x + end + + return wrap(zip_gen, param, state) +end + +Iterator.zip = zip +Iterator.__div = zip +exports.zip = zip + +return exports diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/job.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/job.lua new file mode 100644 index 0000000..7f54971 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/job.lua @@ -0,0 +1,678 @@ +local vim = vim +local uv = vim.loop +local compat = require "plenary.compat" + +local F = require "plenary.functional" + +---@class Job +---@field command string Command to run +---@field args? string[] List of arguments to pass +---@field cwd? string Working directory for job +---@field env? table|string[] Environment looking like: { ['VAR'] = 'VALUE' } or { 'VAR=VALUE' } +---@field interactive? boolean +---@field detached? boolean Spawn the child in a detached state making it a process group leader +---@field skip_validation? boolean Skip validating the arguments +---@field enable_handlers? boolean If set to false, disables all callbacks associated with output (default: true) +---@field enable_recording? boolean +---@field on_start? fun() +---@field on_stdout? fun(error: string, data: string, self?: Job) +---@field on_stderr? fun(error: string, data: string, self?: Job) +---@field on_exit? fun(self: Job, code: number, signal: number) +---@field maximum_results? number Stop processing results after this number +---@field writer? Job|table|string Job that writes to stdin of this job. +local Job = {} +Job.__index = Job + +local function close_safely(j, key) + local handle = j[key] + + if not handle then + return + end + + if not handle:is_closing() then + handle:close() + end +end + +local start_shutdown_check = function(child, options, code, signal) + uv.check_start(child._shutdown_check, function() + if not child:_pipes_are_closed(options) then + return + end + + -- Wait until all the pipes are closing. + uv.check_stop(child._shutdown_check) + child._shutdown_check = nil + + child:_shutdown(code, signal) + + -- Remove left over references + child = nil + end) +end + +local shutdown_factory = function(child, options) + return function(code, signal) + if uv.is_closing(child._shutdown_check) then + return child:shutdown(code, signal) + else + start_shutdown_check(child, options, code, signal) + end + end +end + +local function expand(path) + if vim.in_fast_event() then + return assert(uv.fs_realpath(path), string.format("Path must be valid: %s", path)) + else + -- TODO: Probably want to check that this is valid here... otherwise that's weird. + return vim.fn.expand(vim.fn.escape(path, "[]$"), true) + end +end + +---@class Array +--- Numeric table + +---@class Map +--- Map-like table + +---Create a new job +---@param o Job +---@return Job +function Job:new(o) + if not o then + error(debug.traceback "Options are required for Job:new") + end + + local command = o.command + if not command then + if o[1] then + command = o[1] + else + error(debug.traceback "'command' is required for Job:new") + end + elseif o[1] then + error(debug.traceback "Cannot pass both 'command' and array args") + end + + local args = o.args + if not args then + if #o > 1 then + args = { select(2, unpack(o)) } + end + end + + local ok, is_exe = pcall(vim.fn.executable, command) + if not o.skip_validation and ok and 1 ~= is_exe then + error(debug.traceback(command .. ": Executable not found")) + end + + local obj = {} + + obj.command = command + obj.args = args + obj._raw_cwd = o.cwd + if o.env then + if type(o.env) ~= "table" then + error "[plenary.job] env has to be a table" + end + + local transform = {} + for k, v in pairs(o.env) do + if type(k) == "number" then + table.insert(transform, v) + elseif type(k) == "string" then + table.insert(transform, k .. "=" .. tostring(v)) + end + end + obj.env = transform + end + if o.interactive == nil then + obj.interactive = true + else + obj.interactive = o.interactive + end + + if o.detached then + obj.detached = true + end + + -- enable_handlers: Do you want to do ANYTHING with the stdout/stderr of the proc + obj.enable_handlers = F.if_nil(o.enable_handlers, true, o.enable_handlers) + + -- enable_recording: Do you want to record stdout/stderr into a table. + -- Since it cannot be enabled when enable_handlers is false, + -- we try and make sure they are associated correctly. + obj.enable_recording = + F.if_nil(F.if_nil(o.enable_recording, o.enable_handlers, o.enable_recording), true, o.enable_recording) + + if not obj.enable_handlers and obj.enable_recording then + error "[plenary.job] Cannot record items but disable handlers" + end + + obj._user_on_start = o.on_start + obj._user_on_stdout = o.on_stdout + obj._user_on_stderr = o.on_stderr + obj._user_on_exit = o.on_exit + + obj._additional_on_exit_callbacks = {} + + obj._maximum_results = o.maximum_results + + obj.user_data = {} + + obj.writer = o.writer + + self._reset(obj) + + return setmetatable(obj, self) +end + +function Job:_reset() + self.is_shutdown = nil + + if self._shutdown_check and uv.is_active(self._shutdown_check) and not uv.is_closing(self._shutdown_check) then + vim.api.nvim_err_writeln(debug.traceback "We may be memory leaking here. Please report to TJ.") + end + self._shutdown_check = uv.new_check() + + self.stdin = nil + self.stdout = nil + self.stderr = nil + + self._stdout_reader = nil + self._stderr_reader = nil + + if self.enable_recording then + self._stdout_results = {} + self._stderr_results = {} + else + self._stdout_results = nil + self._stderr_results = nil + end +end + +--- Stop a job and close all handles +function Job:_stop() + close_safely(self, "stdin") + close_safely(self, "stderr") + close_safely(self, "stdout") + close_safely(self, "handle") +end + +function Job:_pipes_are_closed(options) + for _, pipe in ipairs { options.stdin, options.stdout, options.stderr } do + if pipe and not uv.is_closing(pipe) then + return false + end + end + + return true +end + +--- Shutdown a job. +function Job:shutdown(code, signal) + if self._shutdown_check and uv.is_active(self._shutdown_check) then + -- shutdown has already started + return + end + + self:_shutdown(code, signal) +end + +function Job:_shutdown(code, signal) + if self.is_shutdown then + return + end + + self.code = code + self.signal = signal + + if self._stdout_reader then + pcall(self._stdout_reader, nil, nil, true) + end + + if self._stderr_reader then + pcall(self._stderr_reader, nil, nil, true) + end + + if self._user_on_exit then + self:_user_on_exit(code, signal) + end + + for _, v in ipairs(self._additional_on_exit_callbacks) do + v(self, code, signal) + end + + if self.stdout then + self.stdout:read_stop() + end + + if self.stderr then + self.stderr:read_stop() + end + + self:_stop() + + self.is_shutdown = true + + self._stdout_reader = nil + self._stderr_reader = nil +end + +function Job:_create_uv_options() + local options = {} + + options.command = self.command + options.args = self.args + options.stdio = { self.stdin, self.stdout, self.stderr } + + if self._raw_cwd then + options.cwd = expand(self._raw_cwd) + end + if self.env then + options.env = self.env + end + + if self.detached then + options.detached = true + end + + return options +end + +local on_output = function(self, result_key, cb) + return coroutine.wrap(function(err, data, is_complete) + local result_index = 1 + + local line, start, result_line, found_newline + + -- We repeat forever as a coroutine so that we can keep calling this. + while true do + if data then + data = data:gsub("\r", "") + + local processed_index = 1 + local data_length = #data + 1 + + repeat + start = string.find(data, "\n", processed_index, true) or data_length + line = string.sub(data, processed_index, start - 1) + found_newline = start ~= data_length + + -- Concat to last line if there was something there already. + -- This happens when "data" is broken into chunks and sometimes + -- the content is sent without any newlines. + if result_line then + -- results[result_index] = results[result_index] .. line + result_line = result_line .. line + + -- Only put in a new line when we actually have new data to split. + -- This is generally only false when we do end with a new line. + -- It prevents putting in a "" to the end of the results. + elseif start ~= processed_index or found_newline then + -- results[result_index] = line + result_line = line + + -- Otherwise, we don't need to do anything. + end + + if found_newline then + if not result_line then + return vim.api.nvim_err_writeln( + "Broken data thing due to: " .. tostring(result_line) .. " " .. tostring(data) + ) + end + + if self.enable_recording then + self[result_key][result_index] = result_line + end + + if cb then + cb(err, result_line, self) + end + + -- Stop processing if we've surpassed the maximum. + if self._maximum_results and result_index > self._maximum_results then + -- Shutdown once we get the chance. + -- Can't call it here, because we'll just keep calling ourselves. + vim.schedule(function() + self:shutdown() + end) + + return + end + + result_index = result_index + 1 + result_line = nil + end + + processed_index = start + 1 + until not found_newline + end + + if self.enable_recording then + self[result_key][result_index] = result_line + end + + -- If we didn't get a newline on the last execute, send the final results. + if cb and is_complete and not found_newline then + cb(err, result_line, self) + end + + if is_complete then + return + end + + err, data, is_complete = coroutine.yield() + end + end) +end + +--- Stop previous execution and add new pipes. +--- Also regenerates pipes of writer. +function Job:_prepare_pipes() + self:_stop() + + if self.writer then + if Job.is_job(self.writer) then + self.writer:_prepare_pipes() + self.stdin = self.writer.stdout + elseif self.writer.write then + self.stdin = self.writer + end + end + + if not self.stdin then + self.stdin = self.interactive and uv.new_pipe(false) or nil + end + + self.stdout = uv.new_pipe(false) + self.stderr = uv.new_pipe(false) +end + +--- Execute job. Should be called only after preprocessing is done. +function Job:_execute() + local options = self:_create_uv_options() + + if self._user_on_start then + self:_user_on_start() + end + + self.handle, self.pid = uv.spawn(options.command, options, shutdown_factory(self, options)) + + if not self.handle then + error(debug.traceback("Failed to spawn process: " .. vim.inspect(self))) + end + + if self.enable_handlers then + self._stdout_reader = on_output(self, "_stdout_results", self._user_on_stdout) + self.stdout:read_start(self._stdout_reader) + + self._stderr_reader = on_output(self, "_stderr_results", self._user_on_stderr) + self.stderr:read_start(self._stderr_reader) + end + + if self.writer then + if Job.is_job(self.writer) then + self.writer:_execute() + elseif type(self.writer) == "table" and compat.islist(self.writer) then + local writer_len = #self.writer + for i, v in ipairs(self.writer) do + self.stdin:write(v) + if i ~= writer_len then + self.stdin:write "\n" + else + self.stdin:write("\n", function() + pcall(self.stdin.close, self.stdin) + end) + end + end + elseif type(self.writer) == "string" then + self.stdin:write(self.writer, function() + self.stdin:close() + end) + elseif self.writer.write then + self.stdin = self.writer + else + error("Unknown self.writer: " .. vim.inspect(self.writer)) + end + end + + return self +end + +function Job:start() + self:_reset() + self:_prepare_pipes() + self:_execute() +end + +function Job:sync(timeout, wait_interval) + self:start() + self:wait(timeout, wait_interval) + + return self.enable_recording and self:result() or nil, self.code +end + +function Job:result() + assert(self.enable_recording, "'enable_recording' is not enabled for this job.") + return self._stdout_results +end + +function Job:stderr_result() + assert(self.enable_recording, "'enable_recording' is not enabled for this job.") + return self._stderr_results +end + +function Job:pid() + return self.pid +end + +function Job:wait(timeout, wait_interval, should_redraw) + timeout = timeout or 5000 + wait_interval = wait_interval or 10 + + if self.handle == nil then + local msg = vim.inspect(self) + vim.schedule(function() + vim.api.nvim_err_writeln(msg) + end) + + return + end + + -- Wait five seconds, or until timeout. + local wait_result = vim.wait(timeout, function() + if should_redraw then + vim.cmd [[redraw!]] + end + + if self.is_shutdown then + assert(not self.handle or self.handle:is_closing(), "Job must be shutdown if it's closing") + end + + return self.is_shutdown + end, wait_interval, not should_redraw) + + if not wait_result then + error( + string.format( + "'%s %s' was unable to complete in %s ms", + self.command, + table.concat(self.args or {}, " "), + timeout + ) + ) + end + + return self +end + +function Job:co_wait(wait_time) + wait_time = wait_time or 5 + + if self.handle == nil then + vim.api.nvim_err_writeln(vim.inspect(self)) + return + end + + while not vim.wait(wait_time, function() + return self.is_shutdown + end) do + coroutine.yield() + end + + return self +end + +--- Wait for all jobs to complete +function Job.join(...) + local jobs_to_wait = { ... } + local num_jobs = table.getn(jobs_to_wait) + + -- last entry can be timeout + local timeout + if type(jobs_to_wait[num_jobs]) == "number" then + timeout = table.remove(jobs_to_wait, num_jobs) + num_jobs = num_jobs - 1 + end + + local completed = 0 + + return vim.wait(timeout or 10000, function() + for index, current_job in pairs(jobs_to_wait) do + if current_job.is_shutdown then + jobs_to_wait[index] = nil + completed = completed + 1 + end + end + + return num_jobs == completed + end) +end + +local _request_id = 0 +local _request_status = {} + +function Job:and_then(next_job) + self:add_on_exit_callback(function() + next_job:start() + end) +end + +function Job:and_then_wrap(next_job) + self:add_on_exit_callback(vim.schedule_wrap(function() + next_job:start() + end)) +end + +function Job:after(fn) + self:add_on_exit_callback(fn) + return self +end + +function Job:and_then_on_success(next_job) + self:add_on_exit_callback(function(_, code) + if code == 0 then + next_job:start() + end + end) +end + +function Job:and_then_on_success_wrap(next_job) + self:add_on_exit_callback(vim.schedule_wrap(function(_, code) + if code == 0 then + next_job:start() + end + end)) +end + +function Job:after_success(fn) + self:add_on_exit_callback(function(j, code, signal) + if code == 0 then + fn(j, code, signal) + end + end) +end + +function Job:and_then_on_failure(next_job) + self:add_on_exit_callback(function(_, code) + if code ~= 0 then + next_job:start() + end + end) +end + +function Job:and_then_on_failure_wrap(next_job) + self:add_on_exit_callback(vim.schedule_wrap(function(_, code) + if code ~= 0 then + next_job:start() + end + end)) +end + +function Job:after_failure(fn) + self:add_on_exit_callback(function(j, code, signal) + if code ~= 0 then + fn(j, code, signal) + end + end) +end + +function Job.chain(...) + _request_id = _request_id + 1 + _request_status[_request_id] = false + + local jobs = { ... } + + for index = 2, #jobs do + local prev_job = jobs[index - 1] + local job = jobs[index] + + prev_job:add_on_exit_callback(vim.schedule_wrap(function() + job:start() + end)) + end + + local last_on_exit = jobs[#jobs]._user_on_exit + jobs[#jobs]._user_on_exit = function(self, err, data) + if last_on_exit then + last_on_exit(self, err, data) + end + + _request_status[_request_id] = true + end + + jobs[1]:start() + + return _request_id +end + +function Job.chain_status(id) + return _request_status[id] +end + +function Job.is_job(item) + if type(item) ~= "table" then + return false + end + + return getmetatable(item) == Job +end + +function Job:add_on_exit_callback(cb) + table.insert(self._additional_on_exit_callbacks, cb) +end + +--- Send data to a job. +function Job:send(data) + if not self.stdin then + error "job has no 'stdin'. Have you run `job:start()` yet?" + end + + self.stdin:write(data) +end + +return Job diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/json.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/json.lua new file mode 100644 index 0000000..05581f7 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/json.lua @@ -0,0 +1,116 @@ +-- based on https://github.com/sindresorhus/strip-json-comments + +local singleComment = "singleComment" +local multiComment = "multiComment" +local stripWithoutWhitespace = function() + return "" +end + +local function slice(str, from, to) + from = from or 1 + to = to or #str + return str:sub(from, to) +end + +local stripWithWhitespace = function(str, from, to) + return slice(str, from, to):gsub("%S", " ") +end + +local isEscaped = function(jsonString, quotePosition) + local index = quotePosition - 1 + local backslashCount = 0 + + while jsonString:sub(index, index) == "\\" do + index = index - 1 + backslashCount = backslashCount + 1 + end + return backslashCount % 2 == 1 and true or false +end + +local M = {} + +-- Strips any json comments from a json string. +-- The resulting string can then be used by `vim.fn.json_decode` +-- +---@param jsonString string +---@param options? table +--- * whitespace: +--- - defaults to true +--- - when true, comments will be replaced by whitespace +--- - when false, comments will be stripped +--- * trailing_commas: +--- - defaults to false +--- - when true, trailing commas will be included +--- - when false, trailing commas will be removed +function M.json_strip_comments(jsonString, options) + options = options or {} + local strip = options.whitespace == false and stripWithoutWhitespace or stripWithWhitespace + local omitTrailingCommas = not options.trailing_commas + + local insideString = false + local insideComment = false + local offset = 1 + local result = "" + local skip = false + local lastComma = 0 + + for i = 1, #jsonString, 1 do + if skip then + skip = false + else + local currentCharacter = jsonString:sub(i, i) + local nextCharacter = jsonString:sub(i + 1, i + 1) + + if not insideComment and currentCharacter == '"' then + local escaped = isEscaped(jsonString, i) + if not escaped then + insideString = not insideString + end + end + + if not insideString then + if not insideComment and currentCharacter .. nextCharacter == "//" then + result = result .. slice(jsonString, offset, i - 1) + offset = i + insideComment = singleComment + skip = true + elseif insideComment == singleComment and currentCharacter .. nextCharacter == "\r\n" then + i = i + 1 + skip = true + insideComment = false + result = result .. strip(jsonString, offset, i - 1) + offset = i + elseif insideComment == singleComment and currentCharacter == "\n" then + insideComment = false + result = result .. strip(jsonString, offset, i - 1) + offset = i + elseif not insideComment and currentCharacter .. nextCharacter == "/*" then + result = result .. slice(jsonString, offset, i - 1) + offset = i + insideComment = multiComment + skip = true + elseif insideComment == multiComment and currentCharacter .. nextCharacter == "*/" then + i = i + 1 + skip = true + insideComment = false + result = result .. strip(jsonString, offset, i) + offset = i + 1 + elseif omitTrailingCommas and not insideComment then + if currentCharacter == "," then + lastComma = i + elseif (currentCharacter == "]" or currentCharacter == "}") and lastComma > 0 then + result = result .. slice(jsonString, offset, lastComma - 1) .. slice(jsonString, lastComma + 1, i) + offset = i + 1 + lastComma = 0 + elseif currentCharacter:match "%S" then + lastComma = 0 + end + end + end + end + end + + return result .. (insideComment and strip(slice(jsonString, offset)) or slice(jsonString, offset)) +end + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/log.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/log.lua new file mode 100644 index 0000000..d7d3334 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/log.lua @@ -0,0 +1,235 @@ +-- log.lua +-- Does only support logging source files. +-- +-- Inspired by rxi/log.lua +-- Modified by tjdevries and can be found at github.com/tjdevries/vlog.nvim +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. + +local Path = require "plenary.path" + +local p_debug = vim.fn.getenv "DEBUG_PLENARY" +if p_debug == vim.NIL then + p_debug = false +end + +-- User configuration section +local default_config = { + -- Name of the plugin. Prepended to log messages. + plugin = "plenary", + + -- Should print the output to neovim while running. + -- values: 'sync','async',false + use_console = "async", + + -- Should highlighting be used in console (using echohl). + highlights = true, + + -- Should write to a file. + -- Default output for logging file is `stdpath("log")/plugin.log`. + use_file = true, + + -- Output file has precedence over plugin, if not nil. + -- Used for the logging file, if not nil and use_file == true. + outfile = nil, + + -- Should write to the quickfix list. + use_quickfix = false, + + -- Any messages above this level will be logged. + level = p_debug and "debug" or "info", + + -- Level configuration. + modes = { + { name = "trace", hl = "Comment" }, + { name = "debug", hl = "Comment" }, + { name = "info", hl = "None" }, + { name = "warn", hl = "WarningMsg" }, + { name = "error", hl = "ErrorMsg" }, + { name = "fatal", hl = "ErrorMsg" }, + }, + + -- Can limit the number of decimals displayed for floats. + float_precision = 0.01, + + -- Adjust content as needed, but must keep function parameters to be filled + -- by library code. + ---@param is_console boolean If output is for console or log file. + ---@param mode_name string Level configuration 'modes' field 'name' + ---@param src_path string Path to source file given by debug.info.source + ---@param src_line integer Line into source file given by debug.info.currentline + ---@param msg string Message, which is later on escaped, if needed. + fmt_msg = function(is_console, mode_name, src_path, src_line, msg) + local nameupper = mode_name:upper() + local lineinfo = src_path .. ":" .. src_line + if is_console then + return string.format("[%-6s%s] %s: %s", nameupper, os.date "%H:%M:%S", lineinfo, msg) + else + return string.format("[%-6s%s] %s: %s\n", nameupper, os.date(), lineinfo, msg) + end + end, +} + +-- {{{ NO NEED TO CHANGE +local log = {} + +local unpack = unpack or table.unpack + +log.new = function(config, standalone) + config = vim.tbl_deep_extend("force", default_config, config) + + local outfile = vim.F.if_nil( + config.outfile, + Path:new(vim.api.nvim_call_function("stdpath", { "log" }), config.plugin .. ".log").filename + ) + + local obj + if standalone then + obj = log + else + obj = config + end + + local levels = {} + for i, v in ipairs(config.modes) do + levels[v.name] = i + end + + local round = function(x, increment) + if x == 0 then + return x + end + increment = increment or 1 + x = x / increment + return (x > 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)) * increment + end + + local make_string = function(...) + local t = {} + for i = 1, select("#", ...) do + local x = select(i, ...) + + if type(x) == "number" and config.float_precision then + x = tostring(round(x, config.float_precision)) + elseif type(x) == "table" then + x = vim.inspect(x) + else + x = tostring(x) + end + + t[#t + 1] = x + end + return table.concat(t, " ") + end + + local log_at_level = function(level, level_config, message_maker, ...) + -- Return early if we're below the config.level + if level < levels[config.level] then + return + end + local msg = message_maker(...) + local info = debug.getinfo(config.info_level or 2, "Sl") + local src_path = info.source:sub(2) + local src_line = info.currentline + -- Output to console + if config.use_console then + local log_to_console = function() + local console_string = config.fmt_msg(true, level_config.name, src_path, src_line, msg) + + if config.highlights and level_config.hl then + vim.cmd(string.format("echohl %s", level_config.hl)) + end + + local split_console = vim.split(console_string, "\n") + for _, v in ipairs(split_console) do + local formatted_msg = string.format("[%s] %s", config.plugin, vim.fn.escape(v, [["\]])) + + local ok = pcall(vim.cmd, string.format([[echom "%s"]], formatted_msg)) + if not ok then + vim.api.nvim_out_write(msg .. "\n") + end + end + + if config.highlights and level_config.hl then + vim.cmd "echohl NONE" + end + end + if config.use_console == "sync" and not vim.in_fast_event() then + log_to_console() + else + vim.schedule(log_to_console) + end + end + + -- Output to log file + if config.use_file then + local outfile_parent_path = Path:new(outfile):parent() + if not outfile_parent_path:exists() then + outfile_parent_path:mkdir { parents = true } + end + local fp = assert(io.open(outfile, "a")) + local str = config.fmt_msg(false, level_config.name, src_path, src_line, msg) + fp:write(str) + fp:close() + end + + -- Output to quickfix + if config.use_quickfix then + local nameupper = level_config.name:upper() + local formatted_msg = string.format("[%s] %s", nameupper, msg) + local qf_entry = { + -- remove the @ getinfo adds to the file path + filename = info.source:sub(2), + lnum = info.currentline, + col = 1, + text = formatted_msg, + } + vim.fn.setqflist({ qf_entry }, "a") + end + end + + for i, x in ipairs(config.modes) do + -- log.info("these", "are", "separated") + obj[x.name] = function(...) + return log_at_level(i, x, make_string, ...) + end + + -- log.fmt_info("These are %s strings", "formatted") + obj[("fmt_%s"):format(x.name)] = function(...) + return log_at_level(i, x, function(...) + local passed = { ... } + local fmt = table.remove(passed, 1) + local inspected = {} + for _, v in ipairs(passed) do + table.insert(inspected, vim.inspect(v)) + end + return string.format(fmt, unpack(inspected)) + end, ...) + end + + -- log.lazy_info(expensive_to_calculate) + obj[("lazy_%s"):format(x.name)] = function() + return log_at_level(i, x, function(f) + return f() + end) + end + + -- log.file_info("do not print") + obj[("file_%s"):format(x.name)] = function(vals, override) + local original_console = config.use_console + config.use_console = false + config.info_level = override.info_level + log_at_level(i, x, make_string, unpack(vals)) + config.use_console = original_console + config.info_level = nil + end + end + + return obj +end + +log.new(default_config, true) +-- }}} + +return log diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/lsp/override.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/lsp/override.lua new file mode 100644 index 0000000..76bf02a --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/lsp/override.lua @@ -0,0 +1,30 @@ +local vim = vim + +local M = {} + +M._original_functions = {} + +--- Override an lsp method default callback +--- @param method string +--- @param new_function function +function M.override(method, new_function) + if M._original_functions[method] == nil then + M._original_functions[method] = vim.lsp.callbacks[method] + end + + vim.lsp.callbacks[method] = new_function +end + +--- Get the original method callback +--- useful if you only want to override in some circumstances +--- +--- @param method string +function M.get_original_function(method) + if M._original_functions[method] == nil then + M._original_functions[method] = vim.lsp.callbacks[method] + end + + return M._original_functions[method] +end + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/neorocks/init.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/neorocks/init.lua new file mode 100644 index 0000000..df95ac3 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/neorocks/init.lua @@ -0,0 +1 @@ +error "neorocks is no longer supported. Please use packer.nvim or other project for neorocks usage." diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/nvim_meta.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/nvim_meta.lua new file mode 100644 index 0000000..12d9e81 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/nvim_meta.lua @@ -0,0 +1,18 @@ +local get_lua_version = function() + if jit then + return { + lua = string.gsub(_VERSION, "Lua ", ""), + jit = not not string.find(jit.version, "LuaJIT"), + version = string.gsub(jit.version, "LuaJIT ", ""), + } + end + + error("NEOROCKS: Unsupported Lua Versions", _VERSION) +end + +return { + -- Is run in `--headless` mode. + is_headless = (#vim.api.nvim_list_uis() == 0), + + lua_jit = get_lua_version(), +} diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/operators.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/operators.lua new file mode 100644 index 0000000..ccc00c9 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/operators.lua @@ -0,0 +1,100 @@ +---@brief [[ +---Operators that are functions. +---This is useful when you want to pass operators to higher order functions. +---Lua has no currying so we have to make a function for each operator. +---@brief ]] + +return { + ---------------------------------------------------------------------------- + -- Comparison operators + ---------------------------------------------------------------------------- + lt = function(a, b) + return a < b + end, + le = function(a, b) + return a <= b + end, + eq = function(a, b) + return a == b + end, + ne = function(a, b) + return a ~= b + end, + ge = function(a, b) + return a >= b + end, + gt = function(a, b) + return a > b + end, + + ---------------------------------------------------------------------------- + -- Arithmetic operators + ---------------------------------------------------------------------------- + add = function(a, b) + return a + b + end, + div = function(a, b) + return a / b + end, + floordiv = function(a, b) + return math.floor(a / b) + end, + intdiv = function(a, b) + local q = a / b + if a >= 0 then + return math.floor(q) + else + return math.ceil(q) + end + end, + mod = function(a, b) + return a % b + end, + mul = function(a, b) + return a * b + end, + neq = function(a) + return -a + end, + unm = function(a) + return -a + end, -- an alias + pow = function(a, b) + return a ^ b + end, + sub = function(a, b) + return a - b + end, + truediv = function(a, b) + return a / b + end, + + ---------------------------------------------------------------------------- + -- String operators + ---------------------------------------------------------------------------- + concat = function(a, b) + return a .. b + end, + len = function(a) + return #a + end, + length = function(a) + return #a + end, -- an alias + + ---------------------------------------------------------------------------- + -- Logical operators + ---------------------------------------------------------------------------- + land = function(a, b) + return a and b + end, + lor = function(a, b) + return a or b + end, + lnot = function(a) + return not a + end, + truth = function(a) + return not not a + end, +} diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/path.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/path.lua new file mode 100644 index 0000000..0865f2e --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/path.lua @@ -0,0 +1,947 @@ +--- Path.lua +--- +--- Goal: Create objects that are extremely similar to Python's `Path` Objects. +--- Reference: https://docs.python.org/3/library/pathlib.html + +local bit = require "plenary.bit" +local uv = vim.loop + +local F = require "plenary.functional" + +local S_IF = { + -- S_IFDIR = 0o040000 # directory + DIR = 0x4000, + -- S_IFREG = 0o100000 # regular file + REG = 0x8000, +} + +local path = {} +path.home = vim.loop.os_homedir() + +path.sep = (function() + if jit then + local os = string.lower(jit.os) + if os ~= "windows" then + return "/" + else + return "\\" + end + else + return package.config:sub(1, 1) + end +end)() + +path.root = (function() + if path.sep == "/" then + return function() + return "/" + end + else + return function(base) + base = base or vim.loop.cwd() + return base:sub(1, 1) .. ":\\" + end + end +end)() + +path.S_IF = S_IF + +local band = function(reg, value) + return bit.band(reg, value) == reg +end + +local concat_paths = function(...) + return table.concat({ ... }, path.sep) +end + +local function is_root(pathname) + if path.sep == "\\" then + return string.match(pathname, "^[A-Z]:\\?$") + end + return pathname == "/" +end + +local _split_by_separator = (function() + local formatted = string.format("([^%s]+)", path.sep) + return function(filepath) + local t = {} + for str in string.gmatch(filepath, formatted) do + table.insert(t, str) + end + return t + end +end)() + +local is_uri = function(filename) + return string.match(filename, "^%a[%w+-.]*://") ~= nil +end + +local is_absolute = function(filename, sep) + if sep == "\\" then + return string.match(filename, "^[%a]:[\\/].*$") ~= nil + end + return string.sub(filename, 1, 1) == sep +end + +local function _normalize_path(filename, cwd) + if is_uri(filename) then + return filename + end + + -- handles redundant `./` in the middle + local redundant = path.sep .. "%." .. path.sep + if filename:match(redundant) then + filename = filename:gsub(redundant, path.sep) + end + + local out_file = filename + + local has = string.find(filename, path.sep .. "..", 1, true) or string.find(filename, ".." .. path.sep, 1, true) + + if has then + local is_abs = is_absolute(filename, path.sep) + local split_without_disk_name = function(filename_local) + local parts = _split_by_separator(filename_local) + -- Remove disk name part on Windows + if path.sep == "\\" and is_abs then + table.remove(parts, 1) + end + return parts + end + + local parts = split_without_disk_name(filename) + local idx = 1 + local initial_up_count = 0 + + repeat + if parts[idx] == ".." then + if idx == 1 then + initial_up_count = initial_up_count + 1 + end + table.remove(parts, idx) + table.remove(parts, idx - 1) + if idx > 1 then + idx = idx - 2 + else + idx = idx - 1 + end + end + idx = idx + 1 + until idx > #parts + + local prefix = "" + if is_abs or #split_without_disk_name(cwd) == initial_up_count then + prefix = path.root(filename) + end + + out_file = prefix .. table.concat(parts, path.sep) + end + + return out_file +end + +local clean = function(pathname) + if is_uri(pathname) then + return pathname + end + + -- Remove double path seps, it's annoying + pathname = pathname:gsub(path.sep .. path.sep, path.sep) + + -- Remove trailing path sep if not root + if not is_root(pathname) and pathname:sub(-1) == path.sep then + return pathname:sub(1, -2) + end + return pathname +end + +-- S_IFCHR = 0o020000 # character device +-- S_IFBLK = 0o060000 # block device +-- S_IFIFO = 0o010000 # fifo (named pipe) +-- S_IFLNK = 0o120000 # symbolic link +-- S_IFSOCK = 0o140000 # socket file + +---@class Path +local Path = { + path = path, +} + +local check_self = function(self) + if type(self) == "string" then + return Path:new(self) + end + + return self +end + +Path.__index = function(t, k) + local raw = rawget(Path, k) + if raw then + return raw + end + + if k == "_cwd" then + local cwd = uv.fs_realpath "." + t._cwd = cwd + return cwd + end + + if k == "_absolute" then + local absolute = uv.fs_realpath(t.filename) + t._absolute = absolute + return absolute + end +end + +-- TODO: Could use this to not have to call new... not sure +-- Path.__call = Path:new + +Path.__div = function(self, other) + assert(Path.is_path(self)) + assert(Path.is_path(other) or type(other) == "string") + + return self:joinpath(other) +end + +Path.__tostring = function(self) + return clean(self.filename) +end + +-- TODO: See where we concat the table, and maybe we could make this work. +Path.__concat = function(self, other) + return self.filename .. other +end + +Path.is_path = function(a) + return getmetatable(a) == Path +end + +function Path:new(...) + local args = { ... } + + if type(self) == "string" then + table.insert(args, 1, self) + self = Path -- luacheck: ignore + end + + local path_input + if #args == 1 then + path_input = args[1] + else + path_input = args + end + + -- If we already have a Path, it's fine. + -- Just return it + if Path.is_path(path_input) then + return path_input + end + + -- TODO: Should probably remove and dumb stuff like double seps, periods in the middle, etc. + local sep = path.sep + if type(path_input) == "table" then + sep = path_input.sep or path.sep + path_input.sep = nil + end + + local path_string + if type(path_input) == "table" then + -- TODO: It's possible this could be done more elegantly with __concat + -- But I'm unsure of what we'd do to make that happen + local path_objs = {} + for _, v in ipairs(path_input) do + if Path.is_path(v) then + table.insert(path_objs, v.filename) + else + assert(type(v) == "string") + table.insert(path_objs, v) + end + end + + path_string = table.concat(path_objs, sep) + else + assert(type(path_input) == "string", vim.inspect(path_input)) + path_string = path_input + end + + local obj = { + filename = path_string, + + _sep = sep, + } + + setmetatable(obj, Path) + + return obj +end + +function Path:_fs_filename() + return self:absolute() or self.filename +end + +function Path:_stat() + return uv.fs_stat(self:_fs_filename()) or {} + -- local stat = uv.fs_stat(self:absolute()) + -- if not self._absolute then return {} end + + -- if not self._stat_result then + -- self._stat_result = + -- end + + -- return self._stat_result +end + +function Path:_st_mode() + return self:_stat().mode or 0 +end + +function Path:joinpath(...) + return Path:new(self.filename, ...) +end + +function Path:absolute() + if self:is_absolute() then + return _normalize_path(self.filename, self._cwd) + else + return _normalize_path(self._absolute or table.concat({ self._cwd, self.filename }, self._sep), self._cwd) + end +end + +function Path:exists() + return not vim.tbl_isempty(self:_stat()) +end + +function Path:expand() + if is_uri(self.filename) then + return self.filename + end + + -- TODO support windows + local expanded + if string.find(self.filename, "~") then + expanded = string.gsub(self.filename, "^~", vim.loop.os_homedir()) + elseif string.find(self.filename, "^%.") then + expanded = vim.loop.fs_realpath(self.filename) + if expanded == nil then + expanded = vim.fn.fnamemodify(self.filename, ":p") + end + elseif string.find(self.filename, "%$") then + local rep = string.match(self.filename, "([^%$][^/]*)") + local val = os.getenv(rep) + if val then + expanded = string.gsub(string.gsub(self.filename, rep, val), "%$", "") + else + expanded = nil + end + else + expanded = self.filename + end + return expanded and expanded or error "Path not valid" +end + +function Path:make_relative(cwd) + if is_uri(self.filename) then + return self.filename + end + + self.filename = clean(self.filename) + cwd = clean(F.if_nil(cwd, self._cwd, cwd)) + if self.filename == cwd then + self.filename = "." + else + if cwd:sub(#cwd, #cwd) ~= path.sep then + cwd = cwd .. path.sep + end + + if self.filename:sub(1, #cwd) == cwd then + self.filename = self.filename:sub(#cwd + 1, -1) + end + end + + return self.filename +end + +function Path:normalize(cwd) + if is_uri(self.filename) then + return self.filename + end + + self:make_relative(cwd) + + -- Substitute home directory w/ "~" + -- string.gsub is not useful here because usernames with dashes at the end + -- will be seen as a regexp pattern rather than a raw string + local home = path.home + if string.sub(path.home, -1) ~= path.sep then + home = home .. path.sep + end + local start, finish = string.find(self.filename, home, 1, true) + if start == 1 then + self.filename = "~" .. path.sep .. string.sub(self.filename, (finish + 1), -1) + end + + return _normalize_path(clean(self.filename), self._cwd) +end + +local function shorten_len(filename, len, exclude) + len = len or 1 + exclude = exclude or { -1 } + local exc = {} + + -- get parts in a table + local parts = {} + local empty_pos = {} + for m in (filename .. path.sep):gmatch("(.-)" .. path.sep) do + if m ~= "" then + parts[#parts + 1] = m + else + table.insert(empty_pos, #parts + 1) + end + end + + for _, v in pairs(exclude) do + if v < 0 then + exc[v + #parts + 1] = true + else + exc[v] = true + end + end + + local final_path_components = {} + local count = 1 + for _, match in ipairs(parts) do + if not exc[count] and #match > len then + table.insert(final_path_components, string.sub(match, 1, len)) + else + table.insert(final_path_components, match) + end + table.insert(final_path_components, path.sep) + count = count + 1 + end + + local l = #final_path_components -- so that we don't need to keep calculating length + table.remove(final_path_components, l) -- remove final slash + + -- add back empty positions + for i = #empty_pos, 1, -1 do + table.insert(final_path_components, empty_pos[i], path.sep) + end + + return table.concat(final_path_components) +end + +local shorten = (function() + local fallback = function(filename) + return shorten_len(filename, 1) + end + + if jit and path.sep ~= "\\" then + local ffi = require "ffi" + ffi.cdef [[ + typedef unsigned char char_u; + void shorten_dir(char_u *str); + ]] + local ffi_func = function(filename) + if not filename or is_uri(filename) then + return filename + end + + local c_str = ffi.new("char[?]", #filename + 1) + ffi.copy(c_str, filename) + ffi.C.shorten_dir(c_str) + return ffi.string(c_str) + end + local ok = pcall(ffi_func, "/tmp/path/file.lua") + if ok then + return ffi_func + else + return fallback + end + end + return fallback +end)() + +function Path:shorten(len, exclude) + assert(len ~= 0, "len must be at least 1") + if (len and len > 1) or exclude ~= nil then + return shorten_len(self.filename, len, exclude) + end + return shorten(self.filename) +end + +function Path:mkdir(opts) + opts = opts or {} + + local mode = opts.mode or 448 -- 0700 -> decimal + local parents = F.if_nil(opts.parents, false, opts.parents) + local exists_ok = F.if_nil(opts.exists_ok, true, opts.exists_ok) + + local exists = self:exists() + if not exists_ok and exists then + error("FileExistsError:" .. self:absolute()) + end + + -- fs_mkdir returns nil if folder exists + if not uv.fs_mkdir(self:_fs_filename(), mode) and not exists then + if parents then + local dirs = self:_split() + local processed = "" + for _, dir in ipairs(dirs) do + if dir ~= "" then + local joined = concat_paths(processed, dir) + if processed == "" and self._sep == "\\" then + joined = dir + end + local stat = uv.fs_stat(joined) or {} + local file_mode = stat.mode or 0 + if band(S_IF.REG, file_mode) then + error(string.format("%s is a regular file so we can't mkdir it", joined)) + elseif band(S_IF.DIR, file_mode) then + processed = joined + else + if uv.fs_mkdir(joined, mode) then + processed = joined + else + error("We couldn't mkdir: " .. joined) + end + end + end + end + else + error "FileNotFoundError" + end + end + + return true +end + +function Path:rmdir() + if not self:exists() then + return + end + + uv.fs_rmdir(self:absolute()) +end + +function Path:rename(opts) + opts = opts or {} + if not opts.new_name or opts.new_name == "" then + error "Please provide the new name!" + end + + -- handles `.`, `..`, `./`, and `../` + if opts.new_name:match "^%.%.?/?\\?.+" then + opts.new_name = { + uv.fs_realpath(opts.new_name:sub(1, 3)), + opts.new_name:sub(4, #opts.new_name), + } + end + + local new_path = Path:new(opts.new_name) + + if new_path:exists() then + error "File or directory already exists!" + end + + local status = uv.fs_rename(self:absolute(), new_path:absolute()) + self.filename = new_path.filename + + return status +end + +--- Copy files or folders with defaults akin to GNU's `cp`. +---@param opts table: options to pass to toggling registered actions +---@field destination string|Path: target file path to copy to +---@field recursive bool: whether to copy folders recursively (default: false) +---@field override bool: whether to override files (default: true) +---@field interactive bool: confirm if copy would override; precedes `override` (default: false) +---@field respect_gitignore bool: skip folders ignored by all detected `gitignore`s (default: false) +---@field hidden bool: whether to add hidden files in recursively copying folders (default: true) +---@field parents bool: whether to create possibly non-existing parent dirs of `opts.destination` (default: false) +---@field exists_ok bool: whether ok if `opts.destination` exists, if so folders are merged (default: true) +---@return table {[Path of destination]: bool} indicating success of copy; nested tables constitute sub dirs +function Path:copy(opts) + opts = opts or {} + opts.recursive = F.if_nil(opts.recursive, false, opts.recursive) + opts.override = F.if_nil(opts.override, true, opts.override) + + local dest = opts.destination + -- handles `.`, `..`, `./`, and `../` + if not Path.is_path(dest) then + if type(dest) == "string" and dest:match "^%.%.?/?\\?.+" then + dest = { + uv.fs_realpath(dest:sub(1, 3)), + dest:sub(4, #dest), + } + end + dest = Path:new(dest) + end + -- success is true in case file is copied, false otherwise + local success = {} + if not self:is_dir() then + if opts.interactive and dest:exists() then + vim.ui.select( + { "Yes", "No" }, + { prompt = string.format("Overwrite existing %s?", dest:absolute()) }, + function(_, idx) + success[dest] = uv.fs_copyfile(self:absolute(), dest:absolute(), { excl = idx ~= 1 }) or false + end + ) + else + -- nil: not overriden if `override = false` + success[dest] = uv.fs_copyfile(self:absolute(), dest:absolute(), { excl = not opts.override }) or false + end + return success + end + -- dir + if opts.recursive then + dest:mkdir { + parents = F.if_nil(opts.parents, false, opts.parents), + exists_ok = F.if_nil(opts.exists_ok, true, opts.exists_ok), + } + local scan = require "plenary.scandir" + local data = scan.scan_dir(self.filename, { + respect_gitignore = F.if_nil(opts.respect_gitignore, false, opts.respect_gitignore), + hidden = F.if_nil(opts.hidden, true, opts.hidden), + depth = 1, + add_dirs = true, + }) + for _, entry in ipairs(data) do + local entry_path = Path:new(entry) + local suffix = table.remove(entry_path:_split()) + local new_dest = dest:joinpath(suffix) + -- clear destination as it might be Path table otherwise failing w/ extend + opts.destination = nil + local new_opts = vim.tbl_deep_extend("force", opts, { destination = new_dest }) + -- nil: not overriden if `override = false` + success[new_dest] = entry_path:copy(new_opts) or false + end + return success + else + error(string.format("Warning: %s was not copied as `recursive=false`", self:absolute())) + end +end + +function Path:touch(opts) + opts = opts or {} + + local mode = opts.mode or 420 + local parents = F.if_nil(opts.parents, false, opts.parents) + + if self:exists() then + local new_time = os.time() + uv.fs_utime(self:_fs_filename(), new_time, new_time) + return + end + + if parents then + Path:new(self:parent()):mkdir { parents = true } + end + + local fd = uv.fs_open(self:_fs_filename(), "w", mode) + if not fd then + error("Could not create file: " .. self:_fs_filename()) + end + uv.fs_close(fd) + + return true +end + +function Path:rm(opts) + opts = opts or {} + + local recursive = F.if_nil(opts.recursive, false, opts.recursive) + if recursive then + local scan = require "plenary.scandir" + local abs = self:absolute() + + -- first unlink all files + scan.scan_dir(abs, { + hidden = true, + on_insert = function(file) + uv.fs_unlink(file) + end, + }) + + local dirs = scan.scan_dir(abs, { add_dirs = true, hidden = true }) + -- iterate backwards to clean up remaining dirs + for i = #dirs, 1, -1 do + uv.fs_rmdir(dirs[i]) + end + + -- now only abs is missing + uv.fs_rmdir(abs) + else + uv.fs_unlink(self:absolute()) + end +end + +-- Path:is_* {{{ +function Path:is_dir() + -- TODO: I wonder when this would be better, if ever. + -- return self:_stat().type == 'directory' + + return band(S_IF.DIR, self:_st_mode()) +end + +function Path:is_absolute() + return is_absolute(self.filename, self._sep) +end +-- }}} + +function Path:_split() + return vim.split(self:absolute(), self._sep) +end + +local _get_parent = (function() + local formatted = string.format("^(.+)%s[^%s]+", path.sep, path.sep) + return function(abs_path) + local parent = abs_path:match(formatted) + if parent ~= nil and not parent:find(path.sep) then + return parent .. path.sep + end + return parent + end +end)() + +function Path:parent() + return Path:new(_get_parent(self:absolute()) or path.root(self:absolute())) +end + +function Path:parents() + local results = {} + local cur = self:absolute() + repeat + cur = _get_parent(cur) + table.insert(results, cur) + until not cur + table.insert(results, path.root(self:absolute())) + return results +end + +function Path:is_file() + return self:_stat().type == "file" and true or nil +end + +-- TODO: +-- Maybe I can use libuv for this? +function Path:open() end + +function Path:close() end + +function Path:write(txt, flag, mode) + assert(flag, [[Path:write_text requires a flag! For example: 'w' or 'a']]) + + mode = mode or 438 + + local fd = assert(uv.fs_open(self:_fs_filename(), flag, mode)) + assert(uv.fs_write(fd, txt, -1)) + assert(uv.fs_close(fd)) +end + +-- TODO: Asyncify this and use vim.wait in the meantime. +-- This will allow other events to happen while we're waiting! +function Path:_read() + self = check_self(self) + + local fd = assert(uv.fs_open(self:_fs_filename(), "r", 438)) -- for some reason test won't pass with absolute + local stat = assert(uv.fs_fstat(fd)) + local data = assert(uv.fs_read(fd, stat.size, 0)) + assert(uv.fs_close(fd)) + + return data +end + +function Path:_read_async(callback) + vim.loop.fs_open(self.filename, "r", 438, function(err_open, fd) + if err_open then + print("We tried to open this file but couldn't. We failed with following error message: " .. err_open) + return + end + vim.loop.fs_fstat(fd, function(err_fstat, stat) + assert(not err_fstat, err_fstat) + if stat.type ~= "file" then + return callback "" + end + vim.loop.fs_read(fd, stat.size, 0, function(err_read, data) + assert(not err_read, err_read) + vim.loop.fs_close(fd, function(err_close) + assert(not err_close, err_close) + return callback(data) + end) + end) + end) + end) +end + +function Path:read(callback) + if callback then + return self:_read_async(callback) + end + return self:_read() +end + +function Path:head(lines) + lines = lines or 10 + self = check_self(self) + local chunk_size = 256 + + local fd = uv.fs_open(self:_fs_filename(), "r", 438) + if not fd then + return + end + local stat = assert(uv.fs_fstat(fd)) + if stat.type ~= "file" then + uv.fs_close(fd) + return nil + end + + local data = "" + local index, count = 0, 0 + while count < lines and index < stat.size do + local read_chunk = assert(uv.fs_read(fd, chunk_size, index)) + + local i = 0 + for char in read_chunk:gmatch "." do + if char == "\n" then + count = count + 1 + if count >= lines then + break + end + end + index = index + 1 + i = i + 1 + end + data = data .. read_chunk:sub(1, i) + end + assert(uv.fs_close(fd)) + + -- Remove potential newline at end of file + if data:sub(-1) == "\n" then + data = data:sub(1, -2) + end + + return data +end + +function Path:tail(lines) + lines = lines or 10 + self = check_self(self) + local chunk_size = 256 + + local fd = uv.fs_open(self:_fs_filename(), "r", 438) + if not fd then + return + end + local stat = assert(uv.fs_fstat(fd)) + if stat.type ~= "file" then + uv.fs_close(fd) + return nil + end + + local data = "" + local index, count = stat.size - 1, 0 + while count < lines and index > 0 do + local real_index = index - chunk_size + if real_index < 0 then + chunk_size = chunk_size + real_index + real_index = 0 + end + + local read_chunk = assert(uv.fs_read(fd, chunk_size, real_index)) + + local i = #read_chunk + while i > 0 do + local char = read_chunk:sub(i, i) + if char == "\n" then + count = count + 1 + if count >= lines then + break + end + end + index = index - 1 + i = i - 1 + end + data = read_chunk:sub(i + 1, #read_chunk) .. data + end + assert(uv.fs_close(fd)) + + return data +end + +function Path:readlines() + self = check_self(self) + + local data = self:read() + + data = data:gsub("\r", "") + return vim.split(data, "\n") +end + +function Path:iter() + local data = self:readlines() + local i = 0 + local n = #data + return function() + i = i + 1 + if i <= n then + return data[i] + end + end +end + +function Path:readbyterange(offset, length) + self = check_self(self) + + local fd = uv.fs_open(self:_fs_filename(), "r", 438) + if not fd then + return + end + local stat = assert(uv.fs_fstat(fd)) + if stat.type ~= "file" then + uv.fs_close(fd) + return nil + end + + if offset < 0 then + offset = stat.size + offset + -- Windows fails if offset is < 0 even though offset is defined as signed + -- http://docs.libuv.org/en/v1.x/fs.html#c.uv_fs_read + if offset < 0 then + offset = 0 + end + end + + local data = "" + while #data < length do + local read_chunk = assert(uv.fs_read(fd, length - #data, offset)) + if #read_chunk == 0 then + break + end + data = data .. read_chunk + offset = offset + #read_chunk + end + + assert(uv.fs_close(fd)) + + return data +end + +function Path:find_upwards(filename) + local folder = Path:new(self) + local root = path.root(folder:absolute()) + + while true do + local p = folder:joinpath(filename) + if p:exists() then + return p + end + if folder:absolute() == root then + break + end + folder = folder:parent() + end + return nil +end + +return Path diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/popup/init.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/popup/init.lua new file mode 100644 index 0000000..12d8840 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/popup/init.lua @@ -0,0 +1,490 @@ +--- popup.lua +--- +--- Wrapper to make the popup api from vim in neovim. +--- Hope to get this part merged in at some point in the future. +--- +--- Please make sure to update "POPUP.md" with any changes and/or notes. + +local Border = require "plenary.window.border" +local Window = require "plenary.window" +local utils = require "plenary.popup.utils" + +local if_nil = vim.F.if_nil + +local popup = {} + +popup._pos_map = { + topleft = "NW", + topright = "NE", + botleft = "SW", + botright = "SE", +} + +-- Keep track of hidden popups, so we can load them with popup.show() +popup._hidden = {} + +-- Keep track of popup borders, so we don't have to pass them between functions +popup._borders = {} + +local function dict_default(options, key, default) + if options[key] == nil then + return default[key] + else + return options[key] + end +end + +-- Callbacks to be called later by popup.execute_callback +popup._callbacks = {} + +-- Convert the positional {vim_options} to compatible neovim options and add them to {win_opts} +-- If an option is not given in {vim_options}, fall back to {default_opts} +local function add_position_config(win_opts, vim_options, default_opts) + default_opts = default_opts or {} + + local cursor_relative_pos = function(pos_str, dim) + assert(string.find(pos_str, "^cursor"), "Invalid value for " .. dim) + win_opts.relative = "cursor" + local line = 0 + if (pos_str):match "cursor%+(%d+)" then + line = line + tonumber((pos_str):match "cursor%+(%d+)") + elseif (pos_str):match "cursor%-(%d+)" then + line = line - tonumber((pos_str):match "cursor%-(%d+)") + end + return line + end + + -- Feels like maxheight, minheight, maxwidth, minwidth will all be related + -- + -- maxheight Maximum height of the contents, excluding border and padding. + -- minheight Minimum height of the contents, excluding border and padding. + -- maxwidth Maximum width of the contents, excluding border, padding and scrollbar. + -- minwidth Minimum width of the contents, excluding border, padding and scrollbar. + local width = if_nil(vim_options.width, default_opts.width) + local height = if_nil(vim_options.height, default_opts.height) + win_opts.width = utils.bounded(width, vim_options.minwidth, vim_options.maxwidth) + win_opts.height = utils.bounded(height, vim_options.minheight, vim_options.maxheight) + + if vim_options.line and vim_options.line ~= 0 then + if type(vim_options.line) == "string" then + win_opts.row = cursor_relative_pos(vim_options.line, "row") + else + win_opts.row = vim_options.line - 1 + end + else + win_opts.row = math.floor((vim.o.lines - win_opts.height) / 2) + end + + if vim_options.col and vim_options.col ~= 0 then + if type(vim_options.col) == "string" then + win_opts.col = cursor_relative_pos(vim_options.col, "col") + else + win_opts.col = vim_options.col - 1 + end + else + win_opts.col = math.floor((vim.o.columns - win_opts.width) / 2) + end + + -- pos + -- + -- Using "topleft", "topright", "botleft", "botright" defines what corner of the popup "line" + -- and "col" are used for. When not set "topleft" behaviour is used. + -- Alternatively "center" can be used to position the popup in the center of the Neovim window, + -- in which case "line" and "col" are ignored. + if vim_options.pos then + if vim_options.pos == "center" then + vim_options.line = 0 + vim_options.col = 0 + win_opts.anchor = "NW" + else + win_opts.anchor = popup._pos_map[vim_options.pos] + end + else + win_opts.anchor = "NW" -- This is the default, but makes `posinvert` easier to implement + end + + -- , fixed When FALSE (the default), and: + -- , - "pos" is "botleft" or "topleft", and + -- , - "wrap" is off, and + -- , - the popup would be truncated at the right edge of + -- , the screen, then + -- , the popup is moved to the left so as to fit the + -- , contents on the screen. Set to TRUE to disable this. +end + +function popup.create(what, vim_options) + vim_options = vim.deepcopy(vim_options) + + local bufnr + if type(what) == "number" then + bufnr = what + else + bufnr = vim.api.nvim_create_buf(false, true) + assert(bufnr, "Failed to create buffer") + + vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe") + vim.api.nvim_buf_set_option(bufnr, "modifiable", true) + + -- TODO: Handle list of lines + if type(what) == "string" then + what = { what } + else + assert(type(what) == "table", '"what" must be a table') + end + + -- padding List with numbers, defining the padding + -- above/right/below/left of the popup (similar to CSS). + -- An empty list uses a padding of 1 all around. The + -- padding goes around the text, inside any border. + -- Padding uses the 'wincolor' highlight. + -- Example: [1, 2, 1, 3] has 1 line of padding above, 2 + -- columns on the right, 1 line below and 3 columns on + -- the left. + if vim_options.padding then + local pad_top, pad_right, pad_below, pad_left + if vim.tbl_isempty(vim_options.padding) then + pad_top = 1 + pad_right = 1 + pad_below = 1 + pad_left = 1 + else + local padding = vim_options.padding + pad_top = padding[1] or 0 + pad_right = padding[2] or 0 + pad_below = padding[3] or 0 + pad_left = padding[4] or 0 + end + + local left_padding = string.rep(" ", pad_left) + local right_padding = string.rep(" ", pad_right) + for index = 1, #what do + what[index] = string.format("%s%s%s", left_padding, what[index], right_padding) + end + + for _ = 1, pad_top do + table.insert(what, 1, "") + end + + for _ = 1, pad_below do + table.insert(what, "") + end + end + + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, what) + end + + local option_defaults = { + posinvert = true, + zindex = 50, + } + + vim_options.width = if_nil(vim_options.width, 1) + if type(what) == "number" then + vim_options.height = vim.api.nvim_buf_line_count(what) + else + for _, v in ipairs(what) do + vim_options.width = math.max(vim_options.width, #v) + end + vim_options.height = #what + end + + local win_opts = {} + win_opts.relative = "editor" + win_opts.style = "minimal" + win_opts.border = "none" + + -- Add positional and sizing config to win_opts + add_position_config(win_opts, vim_options, { width = 1, height = 1 }) + + -- posinvert, When FALSE the value of "pos" is always used. When + -- , TRUE (the default) and the popup does not fit + -- , vertically and there is more space on the other side + -- , then the popup is placed on the other side of the + -- , position indicated by "line". + if dict_default(vim_options, "posinvert", option_defaults) then + if win_opts.anchor == "NW" or win_opts.anchor == "NE" then + if win_opts.row + win_opts.height > vim.o.lines and win_opts.row * 2 > vim.o.lines then + -- Don't know why, but this is how vim adjusts it + win_opts.row = win_opts.row - win_opts.height - 2 + end + elseif win_opts.anchor == "SW" or win_opts.anchor == "SE" then + if win_opts.row - win_opts.height < 0 and win_opts.row * 2 < vim.o.lines then + -- Don't know why, but this is how vim adjusts it + win_opts.row = win_opts.row + win_opts.height + 2 + end + end + end + + -- textprop, When present the popup is positioned next to a text + -- , property with this name and will move when the text + -- , property moves. Use an empty string to remove. See + -- , |popup-textprop-pos|. + -- related: + -- textpropwin + -- textpropid + + -- zindex, Priority for the popup, default 50. Minimum value is + -- , 1, maximum value is 32000. + local zindex = dict_default(vim_options, "zindex", option_defaults) + win_opts.zindex = utils.bounded(zindex, 1, 32000) + + -- noautocmd, undocumented vim default per https://github.com/vim/vim/issues/5737 + win_opts.noautocmd = if_nil(vim_options.noautocmd, true) + + -- focusable, + -- vim popups are not focusable windows + win_opts.focusable = if_nil(vim_options.focusable, false) + + local win_id + if vim_options.hidden then + assert(false, "I have not implemented this yet and don't know how") + else + win_id = vim.api.nvim_open_win(bufnr, false, win_opts) + end + + -- Moved, handled after since we need the window ID + if vim_options.moved then + if vim_options.moved == "any" then + vim.lsp.util.close_preview_autocmd({ "CursorMoved", "CursorMovedI" }, win_id) + -- elseif vim_options.moved == "word" then + -- TODO: Handle word, WORD, expr, and the range functions... which seem hard? + end + else + local silent = false + vim.cmd( + string.format( + "autocmd BufDelete %s ++once ++nested :lua require('plenary.window').try_close(%s, true)", + (silent and "") or "", + bufnr, + win_id + ) + ) + end + + if vim_options.time then + local timer = vim.loop.new_timer() + timer:start( + vim_options.time, + 0, + vim.schedule_wrap(function() + Window.try_close(win_id, false) + end) + ) + end + + -- Buffer Options + if vim_options.cursorline then + vim.api.nvim_win_set_option(win_id, "cursorline", true) + end + + if vim_options.wrap ~= nil then + -- set_option wrap should/will trigger autocmd, see https://github.com/neovim/neovim/pull/13247 + if vim_options.noautocmd then + vim.cmd(string.format("noautocmd lua vim.api.nvim_set_option(%s, wrap, %s)", win_id, vim_options.wrap)) + else + vim.api.nvim_win_set_option(win_id, "wrap", vim_options.wrap) + end + end + + -- ===== Not Implemented Options ===== + -- flip: not implemented at the time of writing + -- Mouse: + -- mousemoved: no idea how to do the things with the mouse, so it's an exercise for the reader. + -- drag: mouses are hard + -- resize: mouses are hard + -- close: mouses are hard + -- + -- scrollbar + -- scrollbarhighlight + -- thumbhighlight + + -- tabpage: seems useless + + -- Create border + + local should_show_border = nil + local border_options = {} + + -- border List with numbers, defining the border thickness + -- above/right/below/left of the popup (similar to CSS). + -- Only values of zero and non-zero are recognized. + -- An empty list uses a border all around. + if vim_options.border then + should_show_border = true + + if type(vim_options.border) == "boolean" or vim.tbl_isempty(vim_options.border) then + border_options.border_thickness = Border._default_thickness + elseif #vim_options.border == 4 then + border_options.border_thickness = { + top = utils.bounded(vim_options.border[1], 0, 1), + right = utils.bounded(vim_options.border[2], 0, 1), + bot = utils.bounded(vim_options.border[3], 0, 1), + left = utils.bounded(vim_options.border[4], 0, 1), + } + else + error(string.format("Invalid configuration for border: %s", vim.inspect(vim_options.border))) + end + elseif vim_options.border == false then + should_show_border = false + end + + if (should_show_border == nil or should_show_border) and vim_options.borderchars then + should_show_border = true + + -- borderchars List with characters, defining the character to use + -- for the top/right/bottom/left border. Optionally + -- followed by the character to use for the + -- topleft/topright/botright/botleft corner. + -- Example: ['-', '|', '-', '|', '┌', '┐', '┘', '└'] + -- When the list has one character it is used for all. + -- When the list has two characters the first is used for + -- the border lines, the second for the corners. + -- By default a double line is used all around when + -- 'encoding' is "utf-8" and 'ambiwidth' is "single", + -- otherwise ASCII characters are used. + + local b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft + if vim_options.borderchars == nil then + b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = + "═", "║", "═", "║", "╔", "╗", "╝", "╚" + elseif #vim_options.borderchars == 1 then + local b_char = vim_options.borderchars[1] + b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = + b_char, b_char, b_char, b_char, b_char, b_char, b_char, b_char + elseif #vim_options.borderchars == 2 then + local b_char = vim_options.borderchars[1] + local c_char = vim_options.borderchars[2] + b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = + b_char, b_char, b_char, b_char, c_char, c_char, c_char, c_char + elseif #vim_options.borderchars == 8 then + b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = unpack(vim_options.borderchars) + else + error(string.format 'Not enough arguments for "borderchars"') + end + + border_options.top = b_top + border_options.bot = b_bot + border_options.right = b_right + border_options.left = b_left + border_options.topleft = b_topleft + border_options.topright = b_topright + border_options.botright = b_botright + border_options.botleft = b_botleft + end + + -- title + if vim_options.title then + -- TODO: Check out how title works with weird border combos. + border_options.title = vim_options.title + end + + local border = nil + if should_show_border then + border_options.focusable = vim_options.border_focusable + border_options.highlight = vim_options.borderhighlight and string.format("Normal:%s", vim_options.borderhighlight) + border_options.titlehighlight = vim_options.titlehighlight + border = Border:new(bufnr, win_id, win_opts, border_options) + popup._borders[win_id] = border + end + + if vim_options.highlight then + vim.api.nvim_win_set_option( + win_id, + "winhl", + string.format("Normal:%s,EndOfBuffer:%s", vim_options.highlight, vim_options.highlight) + ) + end + + -- enter + local should_enter = vim_options.enter + if should_enter == nil then + should_enter = true + end + + if should_enter then + -- set focus after border creation so that it's properly placed (especially + -- in relative cursor layout) + if vim_options.noautocmd then + vim.cmd("noautocmd lua vim.api.nvim_set_current_win(" .. win_id .. ")") + else + vim.api.nvim_set_current_win(win_id) + end + end + + -- callback + if vim_options.callback then + popup._callbacks[bufnr] = function() + -- (jbyuki): Giving win_id is pointless here because it's closed right afterwards + -- but it might make more sense once hidden is implemented + local row, _ = unpack(vim.api.nvim_win_get_cursor(win_id)) + vim_options.callback(win_id, what[row]) + vim.api.nvim_win_close(win_id, true) + end + vim.api.nvim_buf_set_keymap( + bufnr, + "n", + "", + 'lua require"plenary.popup".execute_callback(' .. bufnr .. ")", + { noremap = true } + ) + end + + if vim_options.finalize_callback then + vim_options.finalize_callback(win_id, bufnr) + end + + -- TODO: Perhaps there's a way to return an object that looks like a window id, + -- but actually has some extra metadata about it. + -- + -- This would make `hidden` a lot easier to manage + return win_id, { + win_id = win_id, + border = border, + } +end + +-- Move popup with window id {win_id} to the position specified with {vim_options}. +-- {vim_options} may contain the following items that determine the popup position/size: +-- - line +-- - col +-- - height +-- - width +-- - maxheight/minheight +-- - maxwidth/minwidth +-- - pos +-- Unimplemented vim options here include: fixed +function popup.move(win_id, vim_options) + -- Create win_options + local win_opts = {} + win_opts.relative = "editor" + + local current_pos = vim.api.nvim_win_get_position(win_id) + local default_opts = { + width = vim.api.nvim_win_get_width(win_id), + height = vim.api.nvim_win_get_height(win_id), + row = current_pos[1], + col = current_pos[2], + } + + -- Add positional and sizing config to win_opts + add_position_config(win_opts, vim_options, default_opts) + + -- Update content window + vim.api.nvim_win_set_config(win_id, win_opts) + + -- Update border window (if present) + local border = popup._borders[win_id] + if border ~= nil then + border:move(win_opts, border._border_win_options) + end +end + +function popup.execute_callback(bufnr) + if popup._callbacks[bufnr] then + local wrapper = popup._callbacks[bufnr] + wrapper() + popup._callbacks[bufnr] = nil + end +end + +return popup diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/popup/utils.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/popup/utils.lua new file mode 100644 index 0000000..e665e15 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/popup/utils.lua @@ -0,0 +1,33 @@ +local utils = {} + +utils.bounded = function(value, min, max) + min = min or 0 + max = max or math.huge + + if min then + value = math.max(value, min) + end + if max then + value = math.min(value, max) + end + + return value +end + +utils.apply_defaults = function(original, defaults) + if original == nil then + original = {} + end + + original = vim.deepcopy(original) + + for k, v in pairs(defaults) do + if original[k] == nil then + original[k] = v + end + end + + return original +end + +return utils diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/profile.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/profile.lua new file mode 100644 index 0000000..0afc4a7 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/profile.lua @@ -0,0 +1,31 @@ +local profile = {} + +-- bundled version of upstream jit.p until LuaJIT is updated to include +-- https://github.com/LuaJIT/LuaJIT/commit/95140c50010c0557af66dac944403a1a65dd312c +local p = require'plenary.profile.p' + +---start profiling using LuaJIT profiler +---@param out name and path of log file +---@param opts table of options +--- flame (bool, default false) write log in flamegraph format +-- (see https://github.com/jonhoo/inferno) +function profile.start(out, opts) + out = out or "profile.log" + opts = opts or {} + local popts = "10,i1,s,m0" + if opts.flame then popts = popts .. ",G" end + p.start(popts, out) +end + +---stop profiling +profile.stop = p.stop + +function profile.benchmark(iterations, f, ...) + local start_time = vim.loop.hrtime() + for _ = 1, iterations do + f(...) + end + return (vim.loop.hrtime() - start_time) / 1E9 +end + +return profile diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/profile/lua_profiler.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/profile/lua_profiler.lua new file mode 100644 index 0000000..e29e06f --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/profile/lua_profiler.lua @@ -0,0 +1,252 @@ +--[[ Copyright (c) 2018-2020, Charles Mallah ]] +-- Released with MIT License +-- +-- Originally link: +-- https://github.com/charlesmallah/lua-profiler +-- +-- Hopefully will add some better neovim stuff in the future. +-- Shoutout to @clason for finding this. + + +---------------------------------------| +--- Configuration +-- +---------------------------------------| + +local PROFILER_FILENAME = "lua/telescope/profile/lua_profiler.lua" -- Location and name of profiler (to remove itself from reports); +-- e.g. if this is in a 'tool' folder, rename this as: "tool/profiler.lua" + +local EMPTY_TIME = "0.0000" -- Detect empty time, replace with tag below +local emptyToThis = "~" + +local fileWidth = 75 +local funcWidth = 22 +local lineWidth = 6 +local timeWidth = 7 +local relaWidth = 6 +local callWidth = 4 + +local reportSaved = " > Report saved to" +local formatOutputHeader = "| %-"..fileWidth.."s: %-"..funcWidth.."s: %-"..lineWidth.."s: %-"..timeWidth.."s: %-"..relaWidth.."s: %-"..callWidth.."s|\n" +local formatOutputTitle = "%-"..fileWidth.."."..fileWidth.."s: %-"..funcWidth.."."..funcWidth.."s: %-"..lineWidth.."s" -- File / Function / Line count +local formatOutput = "| %s: %-"..timeWidth.."s: %-"..relaWidth.."s: %-"..callWidth.."s|\n" -- Time / Relative / Called +local formatTotalTime = "TOTAL TIME = %f s\n" +local formatFunLine = "%"..(lineWidth - 2).."i" +local formatFunTime = "%04.4f" +local formatFunRelative = "%03.1f" +local formatFunCount = "%"..(callWidth - 1).."i" +local formatHeader = string.format(formatOutputHeader, "FILE", "FUNCTION", "LINE", "TIME", "%", "#") + + +---------------------------------------| +--- Locals +-- +---------------------------------------| + +local module = {} + +local getTime = os.clock +local string = string +local debug = debug +local table = table + +local TABL_REPORT_CACHE = {} +local TABL_REPORTS = {} +local reportCount = 0 +local startTime = 0 +local stopTime = 0 + +local printFun = nil +local verbosePrint = false + +local function functionReport(information) + local src = information.short_src + if src == nil then + src = "" + elseif string.sub(src, #src - 3, #src) == ".lua" then + src = string.sub(src, 1, #src - 4) + end + + local name = information.name + if name == nil then + name = "Anon" + elseif string.sub(name, #name - 1, #name) == "_l" then + name = string.sub(name, 1, #name - 2) + end + + local title = string.format(formatOutputTitle, + src, name, + string.format(formatFunLine, information.linedefined or 0)) + + local funcReport = TABL_REPORT_CACHE[title] + if not funcReport then + funcReport = { + title = string.format(formatOutputTitle, + src, name, + string.format(formatFunLine, information.linedefined or 0)), + count = 0, + timer = 0, + } + TABL_REPORT_CACHE[title] = funcReport + reportCount = reportCount + 1 + TABL_REPORTS[reportCount] = funcReport + end + + return funcReport +end + +local onDebugHook = function(hookType) + local information = debug.getinfo(2, "nS") + if hookType == "call" then + local funcReport = functionReport(information) + funcReport.callTime = getTime() + funcReport.count = funcReport.count + 1 + elseif hookType == "return" then + local funcReport = functionReport(information) + if funcReport.callTime and funcReport.count > 0 then + funcReport.timer = funcReport.timer + (getTime() - funcReport.callTime) + end + end +end + +local function charRepetition(n, character) + local s = "" + character = character or " " + for _ = 1, n do + s = s..character + end + return s +end + +local function singleSearchReturn(str, search) + for _ in string.gmatch(str, search) do + do return true end + end + return false +end + +local divider = charRepetition(#formatHeader - 1, "-").."\n" + + +---------------------------------------| +--- Functions +-- +---------------------------------------| + +--- Attach a print function to the profiler, to receive a single string parameter +-- +function module.attachPrintFunction(fn, verbose) + printFun = fn + if verbose ~= nil then + verbosePrint = verbose + end +end + +--- +-- +function module.start() + TABL_REPORT_CACHE = {} + TABL_REPORTS = {} + reportCount = 0 + startTime = getTime() + stopTime = nil + debug.sethook(onDebugHook, "cr", 0) +end + +--- +-- +function module.stop() + stopTime = getTime() + debug.sethook() +end + +--- Writes the profile report to file +-- +function module.report(filename) + if stopTime == nil then + module.stop() + end + + if reportCount > 0 then + filename = filename or "profiler.log" + table.sort(TABL_REPORTS, function(a, b) return a.timer > b.timer end) + local file = io.open(filename, "w+") + + if reportCount > 0 then + local divide = false + local totalTime = stopTime - startTime + local totalTimeOutput = " > "..string.format(formatTotalTime, totalTime) + + file:write(totalTimeOutput) + if printFun ~= nil then + printFun(totalTimeOutput) + end + + file:write("\n"..divider) + file:write(formatHeader) + file:write(divider) + + for i = 1, reportCount do + local funcReport = TABL_REPORTS[i] + + if funcReport.count > 0 and funcReport.timer <= totalTime then + local printThis = true + + if PROFILER_FILENAME ~= "" then + if singleSearchReturn(funcReport.title, PROFILER_FILENAME) then + printThis = false + end + end + + -- Remove line if not needed + if printThis == true then + if singleSearchReturn(funcReport.title, "[[C]]") then + printThis = false + end + end + + if printThis == true then + local count = string.format(formatFunCount, funcReport.count) + local timer = string.format(formatFunTime, funcReport.timer) + local relTime = string.format(formatFunRelative, (funcReport.timer / totalTime) * 100) + if divide == false and timer == EMPTY_TIME then + file:write(divider) + divide = true + end + + -- Replace + if timer == EMPTY_TIME then + timer = emptyToThis + relTime = emptyToThis + end + + -- Build final line + local outputLine = string.format(formatOutput, funcReport.title, timer, relTime, count) + file:write(outputLine) + + -- This is a verbose print to the printFun, however maybe make this smaller for on screen debug? + if printFun ~= nil and verbosePrint == true then + printFun(outputLine) + end + + end + end + end + + file:write(divider) + + end + + file:close() + + if printFun ~= nil then + printFun(reportSaved.."'"..filename.."'") + end + + end + +end + +--- End +-- +return module diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/profile/memory_profiler.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/profile/memory_profiler.lua new file mode 100644 index 0000000..64476ed --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/profile/memory_profiler.lua @@ -0,0 +1,1072 @@ +-- https://github.com/yaukeywang/LuaMemorySnapshotDump +-- Collect memory reference info. +-- +-- @filename MemoryReferenceInfo.lua +-- @author WangYaoqi +-- @date 2016-02-03 + +-- The global config of the mri. +local cConfig = +{ + m_bAllMemoryRefFileAddTime = true, + m_bSingleMemoryRefFileAddTime = true, + m_bComparedMemoryRefFileAddTime = true +} + +-- Get the format string of date time. +local function FormatDateTimeNow() + local cDateTime = os.date("*t") + local strDateTime = string.format("%04d%02d%02d-%02d%02d%02d", tostring(cDateTime.year), tostring(cDateTime.month), tostring(cDateTime.day), + tostring(cDateTime.hour), tostring(cDateTime.min), tostring(cDateTime.sec)) + return strDateTime +end + +-- Get the string result without overrided __tostring. +local function GetOriginalToStringResult(cObject) + if not cObject then + return "" + end + + local cMt = getmetatable(cObject) + if not cMt then + return tostring(cObject) + end + + -- Check tostring override. + local strName = "" + local cToString = rawget(cMt, "__tostring") + if cToString then + rawset(cMt, "__tostring", nil) + strName = tostring(cObject) + rawset(cMt, "__tostring", cToString) + else + strName = tostring(cObject) + end + + return strName +end + +-- Create a container to collect the mem ref info results. +local function CreateObjectReferenceInfoContainer() + -- Create new container. + local cContainer = {} + + -- Contain [table/function] - [reference count] info. + local cObjectReferenceCount = {} + setmetatable(cObjectReferenceCount, {__mode = "k"}) + + -- Contain [table/function] - [name] info. + local cObjectAddressToName = {} + setmetatable(cObjectAddressToName, {__mode = "k"}) + + -- Set members. + cContainer.m_cObjectReferenceCount = cObjectReferenceCount + cContainer.m_cObjectAddressToName = cObjectAddressToName + + -- For stack info. + cContainer.m_nStackLevel = -1 + cContainer.m_strShortSrc = "None" + cContainer.m_nCurrentLine = -1 + + return cContainer +end + +-- Create a container to collect the mem ref info results from a dumped file. +-- strFilePath - The file path. +local function CreateObjectReferenceInfoContainerFromFile(strFilePath) + -- Create a empty container. + local cContainer = CreateObjectReferenceInfoContainer() + cContainer.m_strShortSrc = strFilePath + + -- Cache ref info. + local cRefInfo = cContainer.m_cObjectReferenceCount + local cNameInfo = cContainer.m_cObjectAddressToName + + -- Read each line from file. + local cFile = assert(io.open(strFilePath, "rb")) + for strLine in cFile:lines() do + local strHeader = string.sub(strLine, 1, 2) + if "--" ~= strHeader then + local _, _, strAddr, strName, strRefCount= string.find(strLine, "(.+)\t(.*)\t(%d+)") + if strAddr then + cRefInfo[strAddr] = strRefCount + cNameInfo[strAddr] = strName + end + end + end + + -- Close and clear file handler. + io.close(cFile) + cFile = nil + + return cContainer +end + +-- Create a container to collect the mem ref info results from a dumped file. +-- strObjectName - The object name you need to collect info. +-- cObject - The object you need to collect info. +local function CreateSingleObjectReferenceInfoContainer(strObjectName, cObject) + -- Create new container. + local cContainer = {} + + -- Contain [address] - [true] info. + local cObjectExistTag = {} + setmetatable(cObjectExistTag, {__mode = "k"}) + + -- Contain [name] - [true] info. + local cObjectAliasName = {} + + -- Contain [access] - [true] info. + local cObjectAccessTag = {} + setmetatable(cObjectAccessTag, {__mode = "k"}) + + -- Set members. + cContainer.m_cObjectExistTag = cObjectExistTag + cContainer.m_cObjectAliasName = cObjectAliasName + cContainer.m_cObjectAccessTag = cObjectAccessTag + + -- For stack info. + cContainer.m_nStackLevel = -1 + cContainer.m_strShortSrc = "None" + cContainer.m_nCurrentLine = -1 + + -- Init with object values. + cContainer.m_strObjectName = strObjectName + cContainer.m_strAddressName = (("string" == type(cObject)) and ("\"" .. tostring(cObject) .. "\"")) or GetOriginalToStringResult(cObject) + cContainer.m_cObjectExistTag[cObject] = true + + return cContainer +end + +-- Collect memory reference info from a root table or function. +-- strName - The root object name that start to search, default is "_G" if leave this to nil. +-- cObject - The root object that start to search, default is _G if leave this to nil. +-- cDumpInfoContainer - The container of the dump result info. +local function CollectObjectReferenceInMemory(strName, cObject, cDumpInfoContainer) + if not cObject then + return + end + + if not strName then + strName = "" + end + + -- Check container. + if (not cDumpInfoContainer) then + cDumpInfoContainer = CreateObjectReferenceInfoContainer() + end + + -- Check stack. + if cDumpInfoContainer.m_nStackLevel > 0 then + local cStackInfo = debug.getinfo(cDumpInfoContainer.m_nStackLevel, "Sl") + if cStackInfo then + cDumpInfoContainer.m_strShortSrc = cStackInfo.short_src + cDumpInfoContainer.m_nCurrentLine = cStackInfo.currentline + end + + cDumpInfoContainer.m_nStackLevel = -1 + end + + -- Get ref and name info. + local cRefInfoContainer = cDumpInfoContainer.m_cObjectReferenceCount + local cNameInfoContainer = cDumpInfoContainer.m_cObjectAddressToName + + local strType = type(cObject) + if "table" == strType then + -- Check table with class name. + if rawget(cObject, "__cname") then + if "string" == type(cObject.__cname) then + strName = strName .. "[class:" .. cObject.__cname .. "]" + end + elseif rawget(cObject, "class") then + if "string" == type(cObject.class) then + strName = strName .. "[class:" .. cObject.class .. "]" + end + elseif rawget(cObject, "_className") then + if "string" == type(cObject._className) then + strName = strName .. "[class:" .. cObject._className .. "]" + end + end + + -- Check if table is _G. + if cObject == _G then + strName = strName .. "[_G]" + end + + -- Get metatable. + local bWeakK = false + local bWeakV = false + local cMt = getmetatable(cObject) + if cMt then + -- Check mode. + local strMode = rawget(cMt, "__mode") + if strMode then + if "k" == strMode then + bWeakK = true + elseif "v" == strMode then + bWeakV = true + elseif "kv" == strMode then + bWeakK = true + bWeakV = true + end + end + end + + -- Add reference and name. + cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1 + if cNameInfoContainer[cObject] then + return + end + + -- Set name. + cNameInfoContainer[cObject] = strName + + -- Dump table key and value. + for k, v in pairs(cObject) do + -- Check key type. + local strKeyType = type(k) + if "table" == strKeyType then + if not bWeakK then + CollectObjectReferenceInMemory(strName .. ".[table:key.table]", k, cDumpInfoContainer) + end + + if not bWeakV then + CollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer) + end + elseif "function" == strKeyType then + if not bWeakK then + CollectObjectReferenceInMemory(strName .. ".[table:key.function]", k, cDumpInfoContainer) + end + + if not bWeakV then + CollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer) + end + elseif "thread" == strKeyType then + if not bWeakK then + CollectObjectReferenceInMemory(strName .. ".[table:key.thread]", k, cDumpInfoContainer) + end + + if not bWeakV then + CollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer) + end + elseif "userdata" == strKeyType then + if not bWeakK then + CollectObjectReferenceInMemory(strName .. ".[table:key.userdata]", k, cDumpInfoContainer) + end + + if not bWeakV then + CollectObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer) + end + else + CollectObjectReferenceInMemory(strName .. "." .. k, v, cDumpInfoContainer) + end + end + + -- Dump metatable. + if cMt then + CollectObjectReferenceInMemory(strName ..".[metatable]", cMt, cDumpInfoContainer) + end + elseif "function" == strType then + -- Get function info. + local cDInfo = debug.getinfo(cObject, "Su") + + -- Write this info. + cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1 + if cNameInfoContainer[cObject] then + return + end + + -- Set name. + cNameInfoContainer[cObject] = strName .. "[line:" .. tostring(cDInfo.linedefined) .. "@file:" .. cDInfo.short_src .. "]" + + -- Get upvalues. + local nUpsNum = cDInfo.nups + for i = 1, nUpsNum do + local strUpName, cUpValue = debug.getupvalue(cObject, i) + local strUpValueType = type(cUpValue) + --print(strUpName, cUpValue) + if "table" == strUpValueType then + CollectObjectReferenceInMemory(strName .. ".[ups:table:" .. strUpName .. "]", cUpValue, cDumpInfoContainer) + elseif "function" == strUpValueType then + CollectObjectReferenceInMemory(strName .. ".[ups:function:" .. strUpName .. "]", cUpValue, cDumpInfoContainer) + elseif "thread" == strUpValueType then + CollectObjectReferenceInMemory(strName .. ".[ups:thread:" .. strUpName .. "]", cUpValue, cDumpInfoContainer) + elseif "userdata" == strUpValueType then + CollectObjectReferenceInMemory(strName .. ".[ups:userdata:" .. strUpName .. "]", cUpValue, cDumpInfoContainer) + end + end + + -- Dump environment table. + local getfenv = debug.getfenv + if getfenv then + local cEnv = getfenv(cObject) + if cEnv then + CollectObjectReferenceInMemory(strName ..".[function:environment]", cEnv, cDumpInfoContainer) + end + end + elseif "thread" == strType then + -- Add reference and name. + cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1 + if cNameInfoContainer[cObject] then + return + end + + -- Set name. + cNameInfoContainer[cObject] = strName + + -- Dump environment table. + local getfenv = debug.getfenv + if getfenv then + local cEnv = getfenv(cObject) + if cEnv then + CollectObjectReferenceInMemory(strName ..".[thread:environment]", cEnv, cDumpInfoContainer) + end + end + + -- Dump metatable. + local cMt = getmetatable(cObject) + if cMt then + CollectObjectReferenceInMemory(strName ..".[thread:metatable]", cMt, cDumpInfoContainer) + end + elseif "userdata" == strType then + -- Add reference and name. + cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1 + if cNameInfoContainer[cObject] then + return + end + + -- Set name. + cNameInfoContainer[cObject] = strName + + -- Dump environment table. + local getfenv = debug.getfenv + if getfenv then + local cEnv = getfenv(cObject) + if cEnv then + CollectObjectReferenceInMemory(strName ..".[userdata:environment]", cEnv, cDumpInfoContainer) + end + end + + -- Dump metatable. + local cMt = getmetatable(cObject) + if cMt then + CollectObjectReferenceInMemory(strName ..".[userdata:metatable]", cMt, cDumpInfoContainer) + end + elseif "string" == strType then + -- Add reference and name. + cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1 + if cNameInfoContainer[cObject] then + return + end + + -- Set name. + cNameInfoContainer[cObject] = strName .. "[" .. strType .. "]" + else + -- For "number" and "boolean". (If you want to dump them, uncomment the followed lines.) + + -- -- Add reference and name. + -- cRefInfoContainer[cObject] = (cRefInfoContainer[cObject] and (cRefInfoContainer[cObject] + 1)) or 1 + -- if cNameInfoContainer[cObject] then + -- return + -- end + + -- -- Set name. + -- cNameInfoContainer[cObject] = strName .. "[" .. strType .. ":" .. tostring(cObject) .. "]" + end +end + +-- Collect memory reference info of a single object from a root table or function. +-- strName - The root object name that start to search, can not be nil. +-- cObject - The root object that start to search, can not be nil. +-- cDumpInfoContainer - The container of the dump result info. +local function CollectSingleObjectReferenceInMemory(strName, cObject, cDumpInfoContainer) + if not cObject then + return + end + + if not strName then + strName = "" + end + + -- Check container. + if (not cDumpInfoContainer) then + cDumpInfoContainer = CreateObjectReferenceInfoContainer() + end + + -- Check stack. + if cDumpInfoContainer.m_nStackLevel > 0 then + local cStackInfo = debug.getinfo(cDumpInfoContainer.m_nStackLevel, "Sl") + if cStackInfo then + cDumpInfoContainer.m_strShortSrc = cStackInfo.short_src + cDumpInfoContainer.m_nCurrentLine = cStackInfo.currentline + end + + cDumpInfoContainer.m_nStackLevel = -1 + end + + local cExistTag = cDumpInfoContainer.m_cObjectExistTag + local cNameAllAlias = cDumpInfoContainer.m_cObjectAliasName + local cAccessTag = cDumpInfoContainer.m_cObjectAccessTag + + local strType = type(cObject) + if "table" == strType then + -- Check table with class name. + if rawget(cObject, "__cname") then + if "string" == type(cObject.__cname) then + strName = strName .. "[class:" .. cObject.__cname .. "]" + end + elseif rawget(cObject, "class") then + if "string" == type(cObject.class) then + strName = strName .. "[class:" .. cObject.class .. "]" + end + elseif rawget(cObject, "_className") then + if "string" == type(cObject._className) then + strName = strName .. "[class:" .. cObject._className .. "]" + end + end + + -- Check if table is _G. + if cObject == _G then + strName = strName .. "[_G]" + end + + -- Get metatable. + local bWeakK = false + local bWeakV = false + local cMt = getmetatable(cObject) + if cMt then + -- Check mode. + local strMode = rawget(cMt, "__mode") + if strMode then + if "k" == strMode then + bWeakK = true + elseif "v" == strMode then + bWeakV = true + elseif "kv" == strMode then + bWeakK = true + bWeakV = true + end + end + end + + -- Check if the specified object. + if cExistTag[cObject] and (not cNameAllAlias[strName]) then + cNameAllAlias[strName] = true + end + + -- Add reference and name. + if cAccessTag[cObject] then + return + end + + -- Get this name. + cAccessTag[cObject] = true + + -- Dump table key and value. + for k, v in pairs(cObject) do + -- Check key type. + local strKeyType = type(k) + if "table" == strKeyType then + if not bWeakK then + CollectSingleObjectReferenceInMemory(strName .. ".[table:key.table]", k, cDumpInfoContainer) + end + + if not bWeakV then + CollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer) + end + elseif "function" == strKeyType then + if not bWeakK then + CollectSingleObjectReferenceInMemory(strName .. ".[table:key.function]", k, cDumpInfoContainer) + end + + if not bWeakV then + CollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer) + end + elseif "thread" == strKeyType then + if not bWeakK then + CollectSingleObjectReferenceInMemory(strName .. ".[table:key.thread]", k, cDumpInfoContainer) + end + + if not bWeakV then + CollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer) + end + elseif "userdata" == strKeyType then + if not bWeakK then + CollectSingleObjectReferenceInMemory(strName .. ".[table:key.userdata]", k, cDumpInfoContainer) + end + + if not bWeakV then + CollectSingleObjectReferenceInMemory(strName .. ".[table:value]", v, cDumpInfoContainer) + end + else + CollectSingleObjectReferenceInMemory(strName .. "." .. k, v, cDumpInfoContainer) + end + end + + -- Dump metatable. + if cMt then + CollectSingleObjectReferenceInMemory(strName ..".[metatable]", cMt, cDumpInfoContainer) + end + elseif "function" == strType then + -- Get function info. + local cDInfo = debug.getinfo(cObject, "Su") + local cCombinedName = strName .. "[line:" .. tostring(cDInfo.linedefined) .. "@file:" .. cDInfo.short_src .. "]" + + -- Check if the specified object. + if cExistTag[cObject] and (not cNameAllAlias[cCombinedName]) then + cNameAllAlias[cCombinedName] = true + end + + -- Write this info. + if cAccessTag[cObject] then + return + end + + -- Set name. + cAccessTag[cObject] = true + + -- Get upvalues. + local nUpsNum = cDInfo.nups + for i = 1, nUpsNum do + local strUpName, cUpValue = debug.getupvalue(cObject, i) + local strUpValueType = type(cUpValue) + --print(strUpName, cUpValue) + if "table" == strUpValueType then + CollectSingleObjectReferenceInMemory(strName .. ".[ups:table:" .. strUpName .. "]", cUpValue, cDumpInfoContainer) + elseif "function" == strUpValueType then + CollectSingleObjectReferenceInMemory(strName .. ".[ups:function:" .. strUpName .. "]", cUpValue, cDumpInfoContainer) + elseif "thread" == strUpValueType then + CollectSingleObjectReferenceInMemory(strName .. ".[ups:thread:" .. strUpName .. "]", cUpValue, cDumpInfoContainer) + elseif "userdata" == strUpValueType then + CollectSingleObjectReferenceInMemory(strName .. ".[ups:userdata:" .. strUpName .. "]", cUpValue, cDumpInfoContainer) + end + end + + -- Dump environment table. + local getfenv = debug.getfenv + if getfenv then + local cEnv = getfenv(cObject) + if cEnv then + CollectSingleObjectReferenceInMemory(strName ..".[function:environment]", cEnv, cDumpInfoContainer) + end + end + elseif "thread" == strType then + -- Check if the specified object. + if cExistTag[cObject] and (not cNameAllAlias[strName]) then + cNameAllAlias[strName] = true + end + + -- Add reference and name. + if cAccessTag[cObject] then + return + end + + -- Get this name. + cAccessTag[cObject] = true + + -- Dump environment table. + local getfenv = debug.getfenv + if getfenv then + local cEnv = getfenv(cObject) + if cEnv then + CollectSingleObjectReferenceInMemory(strName ..".[thread:environment]", cEnv, cDumpInfoContainer) + end + end + + -- Dump metatable. + local cMt = getmetatable(cObject) + if cMt then + CollectSingleObjectReferenceInMemory(strName ..".[thread:metatable]", cMt, cDumpInfoContainer) + end + elseif "userdata" == strType then + -- Check if the specified object. + if cExistTag[cObject] and (not cNameAllAlias[strName]) then + cNameAllAlias[strName] = true + end + + -- Add reference and name. + if cAccessTag[cObject] then + return + end + + -- Get this name. + cAccessTag[cObject] = true + + -- Dump environment table. + local getfenv = debug.getfenv + if getfenv then + local cEnv = getfenv(cObject) + if cEnv then + CollectSingleObjectReferenceInMemory(strName ..".[userdata:environment]", cEnv, cDumpInfoContainer) + end + end + + -- Dump metatable. + local cMt = getmetatable(cObject) + if cMt then + CollectSingleObjectReferenceInMemory(strName ..".[userdata:metatable]", cMt, cDumpInfoContainer) + end + elseif "string" == strType then + -- Check if the specified object. + if cExistTag[cObject] and (not cNameAllAlias[strName]) then + cNameAllAlias[strName] = true + end + + -- Add reference and name. + if cAccessTag[cObject] then + return + end + + -- Get this name. + cAccessTag[cObject] = true + else + -- For "number" and "boolean" type, they are not object type, skip. + end +end + +-- The base method to dump a mem ref info result into a file. +-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does. +-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "". +-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result. +-- strRootObjectName - The header info to show the root object name, can be nil. +-- cRootObject - The header info to show the root object address, can be nil. +-- cDumpInfoResultsBase - The base dumped mem info result, nil means no compare and only output cDumpInfoResults, otherwise to compare with cDumpInfoResults. +-- cDumpInfoResults - The compared dumped mem info result, dump itself only if cDumpInfoResultsBase is nil, otherwise dump compared results with cDumpInfoResultsBase. +local function OutputMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, strRootObjectName, cRootObject, cDumpInfoResultsBase, cDumpInfoResults) + -- Check results. + if not cDumpInfoResults then + return + end + + -- Get time format string. + local strDateTime = FormatDateTimeNow() + + -- Collect memory info. + local cRefInfoBase = (cDumpInfoResultsBase and cDumpInfoResultsBase.m_cObjectReferenceCount) or nil + local cNameInfoBase = (cDumpInfoResultsBase and cDumpInfoResultsBase.m_cObjectAddressToName) or nil + local cRefInfo = cDumpInfoResults.m_cObjectReferenceCount + local cNameInfo = cDumpInfoResults.m_cObjectAddressToName + + -- Create a cache result to sort by ref count. + local cRes = {} + local nIdx = 0 + for k in pairs(cRefInfo) do + nIdx = nIdx + 1 + cRes[nIdx] = k + end + + -- Sort result. + table.sort(cRes, function (l, r) + return cRefInfo[l] > cRefInfo[r] + end) + + -- Save result to file. + local bOutputFile = strSavePath and (string.len(strSavePath) > 0) + local cOutputHandle = nil + local cOutputEntry = print + + if bOutputFile then + -- Check save path affix. + local strAffix = string.sub(strSavePath, -1) + if ("/" ~= strAffix) and ("\\" ~= strAffix) then + strSavePath = strSavePath .. "/" + end + + -- Combine file name. + local strFileName = strSavePath .. "LuaMemRefInfo-All" + if (not strExtraFileName) or (0 == string.len(strExtraFileName)) then + if cDumpInfoResultsBase then + if cConfig.m_bComparedMemoryRefFileAddTime then + strFileName = strFileName .. "-[" .. strDateTime .. "].txt" + else + strFileName = strFileName .. ".txt" + end + else + if cConfig.m_bAllMemoryRefFileAddTime then + strFileName = strFileName .. "-[" .. strDateTime .. "].txt" + else + strFileName = strFileName .. ".txt" + end + end + else + if cDumpInfoResultsBase then + if cConfig.m_bComparedMemoryRefFileAddTime then + strFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt" + else + strFileName = strFileName .. "-[" .. strExtraFileName .. "].txt" + end + else + if cConfig.m_bAllMemoryRefFileAddTime then + strFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt" + else + strFileName = strFileName .. "-[" .. strExtraFileName .. "].txt" + end + end + end + + local cFile = assert(io.open(strFileName, "w")) + cOutputHandle = cFile + cOutputEntry = cFile.write + end + + local cOutputer = function (strContent) + if cOutputHandle then + cOutputEntry(cOutputHandle, strContent) + else + cOutputEntry(strContent) + end + end + + -- Write table header. + if cDumpInfoResultsBase then + cOutputer("--------------------------------------------------------\n") + cOutputer("-- This is compared memory information.\n") + + cOutputer("--------------------------------------------------------\n") + cOutputer("-- Collect base memory reference at line:" .. tostring(cDumpInfoResultsBase.m_nCurrentLine) .. "@file:" .. cDumpInfoResultsBase.m_strShortSrc .. "\n") + cOutputer("-- Collect compared memory reference at line:" .. tostring(cDumpInfoResults.m_nCurrentLine) .. "@file:" .. cDumpInfoResults.m_strShortSrc .. "\n") + else + cOutputer("--------------------------------------------------------\n") + cOutputer("-- Collect memory reference at line:" .. tostring(cDumpInfoResults.m_nCurrentLine) .. "@file:" .. cDumpInfoResults.m_strShortSrc .. "\n") + end + + cOutputer("--------------------------------------------------------\n") + cOutputer("-- [Table/Function/String Address/Name]\t[Reference Path]\t[Reference Count]\n") + cOutputer("--------------------------------------------------------\n") + + if strRootObjectName and cRootObject then + if "string" == type(cRootObject) then + cOutputer("-- From Root Object: \"" .. tostring(cRootObject) .. "\" (" .. strRootObjectName .. ")\n") + else + cOutputer("-- From Root Object: " .. GetOriginalToStringResult(cRootObject) .. " (" .. strRootObjectName .. ")\n") + end + end + + -- Save each info. + for i, v in ipairs(cRes) do + if (not cDumpInfoResultsBase) or (not cRefInfoBase[v]) then + if (nMaxRescords > 0) then + if (i <= nMaxRescords) then + if "string" == type(v) then + local strOrgString = tostring(v) + local nPattenBegin, nPattenEnd = string.find(strOrgString, "string: \".*\"") + if ((not cDumpInfoResultsBase) and ((nil == nPattenBegin) or (nil == nPattenEnd))) then + local strRepString = string.gsub(strOrgString, "([\n\r])", "\\n") + cOutputer("string: \"" .. strRepString .. "\"\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n") + else + cOutputer(tostring(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n") + end + else + cOutputer(GetOriginalToStringResult(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n") + end + end + else + if "string" == type(v) then + local strOrgString = tostring(v) + local nPattenBegin, nPattenEnd = string.find(strOrgString, "string: \".*\"") + if ((not cDumpInfoResultsBase) and ((nil == nPattenBegin) or (nil == nPattenEnd))) then + local strRepString = string.gsub(strOrgString, "([\n\r])", "\\n") + cOutputer("string: \"" .. strRepString .. "\"\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n") + else + cOutputer(tostring(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n") + end + else + cOutputer(GetOriginalToStringResult(v) .. "\t" .. cNameInfo[v] .. "\t" .. tostring(cRefInfo[v]) .. "\n") + end + end + end + end + + if bOutputFile then + io.close(cOutputHandle) + cOutputHandle = nil + end +end + +-- The base method to dump a mem ref info result of a single object into a file. +-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does. +-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "". +-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result. +-- cDumpInfoResults - The dumped results. +local function OutputMemorySnapshotSingleObject(strSavePath, strExtraFileName, nMaxRescords, cDumpInfoResults) + -- Check results. + if not cDumpInfoResults then + return + end + + -- Get time format string. + local strDateTime = FormatDateTimeNow() + + -- Collect memory info. + local cObjectAliasName = cDumpInfoResults.m_cObjectAliasName + + -- Save result to file. + local bOutputFile = strSavePath and (string.len(strSavePath) > 0) + local cOutputHandle = nil + local cOutputEntry = print + + if bOutputFile then + -- Check save path affix. + local strAffix = string.sub(strSavePath, -1) + if ("/" ~= strAffix) and ("\\" ~= strAffix) then + strSavePath = strSavePath .. "/" + end + + -- Combine file name. + local strFileName = strSavePath .. "LuaMemRefInfo-Single" + if (not strExtraFileName) or (0 == string.len(strExtraFileName)) then + if cConfig.m_bSingleMemoryRefFileAddTime then + strFileName = strFileName .. "-[" .. strDateTime .. "].txt" + else + strFileName = strFileName .. ".txt" + end + else + if cConfig.m_bSingleMemoryRefFileAddTime then + strFileName = strFileName .. "-[" .. strDateTime .. "]-[" .. strExtraFileName .. "].txt" + else + strFileName = strFileName .. "-[" .. strExtraFileName .. "].txt" + end + end + + local cFile = assert(io.open(strFileName, "w")) + cOutputHandle = cFile + cOutputEntry = cFile.write + end + + local cOutputer = function (strContent) + if cOutputHandle then + cOutputEntry(cOutputHandle, strContent) + else + cOutputEntry(strContent) + end + end + + -- Write table header. + cOutputer("--------------------------------------------------------\n") + cOutputer("-- Collect single object memory reference at line:" .. tostring(cDumpInfoResults.m_nCurrentLine) .. "@file:" .. cDumpInfoResults.m_strShortSrc .. "\n") + cOutputer("--------------------------------------------------------\n") + + -- Calculate reference count. + local nCount = 0 + for k in pairs(cObjectAliasName) do + nCount = nCount + 1 + end + + -- Output reference count. + cOutputer("-- For Object: " .. cDumpInfoResults.m_strAddressName .. " (" .. cDumpInfoResults.m_strObjectName .. "), have " .. tostring(nCount) .. " reference in total.\n") + cOutputer("--------------------------------------------------------\n") + + -- Save each info. + for k in pairs(cObjectAliasName) do + if (nMaxRescords > 0) then + if (i <= nMaxRescords) then + cOutputer(k .. "\n") + end + else + cOutputer(k .. "\n") + end + end + + if bOutputFile then + io.close(cOutputHandle) + cOutputHandle = nil + end +end + +-- Fileter an existing result file and output it. +-- strFilePath - The existing result file. +-- strFilter - The filter string. +-- bIncludeFilter - Include(true) or exclude(false) the filter. +-- bOutputFile - Output to file(true) or console(false). +local function OutputFilteredResult(strFilePath, strFilter, bIncludeFilter, bOutputFile) + if (not strFilePath) or (0 == string.len(strFilePath)) then + print("You need to specify a file path.") + return + end + + if (not strFilter) or (0 == string.len(strFilter)) then + print("You need to specify a filter string.") + return + end + + -- Read file. + local cFilteredResult = {} + local cReadFile = assert(io.open(strFilePath, "rb")) + for strLine in cReadFile:lines() do + local nBegin, nEnd = string.find(strLine, strFilter) + if nBegin and nEnd then + if bIncludeFilter then + nBegin, nEnd = string.find(strLine, "[\r\n]") + if nBegin and nEnd and (string.len(strLine) == nEnd) then + table.insert(cFilteredResult, string.sub(strLine, 1, nBegin - 1)) + else + table.insert(cFilteredResult, strLine) + end + end + else + if not bIncludeFilter then + nBegin, nEnd = string.find(strLine, "[\r\n]") + if nBegin and nEnd and (string.len(strLine) == nEnd) then + table.insert(cFilteredResult, string.sub(strLine, 1, nBegin - 1)) + else + table.insert(cFilteredResult, strLine) + end + end + end + end + + -- Close and clear read file handle. + io.close(cReadFile) + cReadFile = nil + + -- Write filtered result. + local cOutputHandle = nil + local cOutputEntry = print + + if bOutputFile then + -- Combine file name. + local _, _, strResFileName = string.find(strFilePath, "(.*)%.txt") + strResFileName = strResFileName .. "-Filter-" .. ((bIncludeFilter and "I") or "E") .. "-[" .. strFilter .. "].txt" + + local cFile = assert(io.open(strResFileName, "w")) + cOutputHandle = cFile + cOutputEntry = cFile.write + end + + local cOutputer = function (strContent) + if cOutputHandle then + cOutputEntry(cOutputHandle, strContent) + else + cOutputEntry(strContent) + end + end + + -- Output result. + for i, v in ipairs(cFilteredResult) do + cOutputer(v .. "\n") + end + + if bOutputFile then + io.close(cOutputHandle) + cOutputHandle = nil + end +end + +-- Dump memory reference at current time. +-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does. +-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "". +-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result. +-- strRootObjectName - The root object name that start to search, default is "_G" if leave this to nil. +-- cRootObject - The root object that start to search, default is _G if leave this to nil. +local function DumpMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, strRootObjectName, cRootObject) + -- Get time format string. + local strDateTime = FormatDateTimeNow() + + -- Check root object. + if cRootObject then + if (not strRootObjectName) or (0 == string.len(strRootObjectName)) then + strRootObjectName = tostring(cRootObject) + end + else + cRootObject = debug.getregistry() + strRootObjectName = "registry" + end + + -- Create container. + local cDumpInfoContainer = CreateObjectReferenceInfoContainer() + local cStackInfo = debug.getinfo(2, "Sl") + if cStackInfo then + cDumpInfoContainer.m_strShortSrc = cStackInfo.short_src + cDumpInfoContainer.m_nCurrentLine = cStackInfo.currentline + end + + -- Collect memory info. + CollectObjectReferenceInMemory(strRootObjectName, cRootObject, cDumpInfoContainer) + + -- Dump the result. + OutputMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, strRootObjectName, cRootObject, nil, cDumpInfoContainer) +end + +-- Dump compared memory reference results generated by DumpMemorySnapshot. +-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does. +-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "". +-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result. +-- cResultBefore - The base dumped results. +-- cResultAfter - The compared dumped results. +local function DumpMemorySnapshotCompared(strSavePath, strExtraFileName, nMaxRescords, cResultBefore, cResultAfter) + -- Dump the result. + OutputMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, nil, nil, cResultBefore, cResultAfter) +end + +-- Dump compared memory reference file results generated by DumpMemorySnapshot. +-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does. +-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "". +-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result. +-- strResultFilePathBefore - The base dumped results file. +-- strResultFilePathAfter - The compared dumped results file. +local function DumpMemorySnapshotComparedFile(strSavePath, strExtraFileName, nMaxRescords, strResultFilePathBefore, strResultFilePathAfter) + -- Read results from file. + local cResultBefore = CreateObjectReferenceInfoContainerFromFile(strResultFilePathBefore) + local cResultAfter = CreateObjectReferenceInfoContainerFromFile(strResultFilePathAfter) + + -- Dump the result. + OutputMemorySnapshot(strSavePath, strExtraFileName, nMaxRescords, nil, nil, cResultBefore, cResultAfter) +end + +-- Dump memory reference of a single object at current time. +-- strSavePath - The save path of the file to store the result, must be a directory path, If nil or "" then the result will output to console as print does. +-- strExtraFileName - If you want to add extra info append to the end of the result file, give a string, nothing will do if set to nil or "". +-- nMaxRescords - How many rescords of the results in limit to save in the file or output to the console, -1 will give all the result. +-- strObjectName - The object name reference you want to dump. +-- cObject - The object reference you want to dump. +local function DumpMemorySnapshotSingleObject(strSavePath, strExtraFileName, nMaxRescords, strObjectName, cObject) + -- Check object. + if not cObject then + return + end + + if (not strObjectName) or (0 == string.len(strObjectName)) then + strObjectName = GetOriginalToStringResult(cObject) + end + + -- Get time format string. + local strDateTime = FormatDateTimeNow() + + -- Create container. + local cDumpInfoContainer = CreateSingleObjectReferenceInfoContainer(strObjectName, cObject) + local cStackInfo = debug.getinfo(2, "Sl") + if cStackInfo then + cDumpInfoContainer.m_strShortSrc = cStackInfo.short_src + cDumpInfoContainer.m_nCurrentLine = cStackInfo.currentline + end + + -- Collect memory info. + CollectSingleObjectReferenceInMemory("registry", debug.getregistry(), cDumpInfoContainer) + + -- Dump the result. + OutputMemorySnapshotSingleObject(strSavePath, strExtraFileName, nMaxRescords, cDumpInfoContainer) +end + +-- Return methods. +local cPublications = {m_cConfig = nil, m_cMethods = {}, m_cHelpers = {}, m_cBases = {}} + +cPublications.m_cConfig = cConfig + +cPublications.m_cMethods.DumpMemorySnapshot = DumpMemorySnapshot +cPublications.m_cMethods.DumpMemorySnapshotCompared = DumpMemorySnapshotCompared +cPublications.m_cMethods.DumpMemorySnapshotComparedFile = DumpMemorySnapshotComparedFile +cPublications.m_cMethods.DumpMemorySnapshotSingleObject = DumpMemorySnapshotSingleObject + +cPublications.m_cHelpers.FormatDateTimeNow = FormatDateTimeNow +cPublications.m_cHelpers.GetOriginalToStringResult = GetOriginalToStringResult + +cPublications.m_cBases.CreateObjectReferenceInfoContainer = CreateObjectReferenceInfoContainer +cPublications.m_cBases.CreateObjectReferenceInfoContainerFromFile = CreateObjectReferenceInfoContainerFromFile +cPublications.m_cBases.CreateSingleObjectReferenceInfoContainer = CreateSingleObjectReferenceInfoContainer +cPublications.m_cBases.CollectObjectReferenceInMemory = CollectObjectReferenceInMemory +cPublications.m_cBases.CollectSingleObjectReferenceInMemory = CollectSingleObjectReferenceInMemory +cPublications.m_cBases.OutputMemorySnapshot = OutputMemorySnapshot +cPublications.m_cBases.OutputMemorySnapshotSingleObject = OutputMemorySnapshotSingleObject +cPublications.m_cBases.OutputFilteredResult = OutputFilteredResult + +return cPublications diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/profile/p.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/profile/p.lua new file mode 100644 index 0000000..9eb6f16 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/profile/p.lua @@ -0,0 +1,312 @@ +---------------------------------------------------------------------------- +-- LuaJIT profiler. +-- +-- Copyright (C) 2005-2021 Mike Pall. All rights reserved. +-- Released under the MIT license. See Copyright Notice in luajit.h +---------------------------------------------------------------------------- +-- +-- This module is a simple command line interface to the built-in +-- low-overhead profiler of LuaJIT. +-- +-- The lower-level API of the profiler is accessible via the "jit.profile" +-- module or the luaJIT_profile_* C API. +-- +-- Example usage: +-- +-- luajit -jp myapp.lua +-- luajit -jp=s myapp.lua +-- luajit -jp=-s myapp.lua +-- luajit -jp=vl myapp.lua +-- luajit -jp=G,profile.txt myapp.lua +-- +-- The following dump features are available: +-- +-- f Stack dump: function name, otherwise module:line. Default mode. +-- F Stack dump: ditto, but always prepend module. +-- l Stack dump: module:line. +-- stack dump depth (callee < caller). Default: 1. +-- - Inverse stack dump depth (caller > callee). +-- s Split stack dump after first stack level. Implies abs(depth) >= 2. +-- p Show full path for module names. +-- v Show VM states. Can be combined with stack dumps, e.g. vf or fv. +-- z Show zones. Can be combined with stack dumps, e.g. zf or fz. +-- r Show raw sample counts. Default: show percentages. +-- a Annotate excerpts from source code files. +-- A Annotate complete source code files. +-- G Produce raw output suitable for graphical tools (e.g. flame graphs). +-- m Minimum sample percentage to be shown. Default: 3. +-- i Sampling interval in milliseconds. Default: 10. +-- +---------------------------------------------------------------------------- + +-- Cache some library functions and objects. +local jit = require("jit") +assert(20100 <= jit.version_num and jit.version_num <= 20199, "LuaJIT core/library version mismatch: " .. jit.version) +local profile = require("jit.profile") +local vmdef = require("jit.vmdef") +local math = math +local pairs, ipairs, tonumber, floor = pairs, ipairs, tonumber, math.floor +local sort, format = table.sort, string.format +local stdout = io.stdout +local zone -- Load jit.zone module on demand. + +-- Output file handle. +local out + +------------------------------------------------------------------------------ + +local prof_ud +local prof_states, prof_split, prof_min, prof_raw, prof_fmt, prof_depth +local prof_ann, prof_count1, prof_count2, prof_samples + +local map_vmmode = { + N = "Compiled", + I = "Interpreted", + C = "C code", + G = "Garbage Collector", + J = "JIT Compiler", +} + +-- Profiler callback. +local function prof_cb(th, samples, vmmode) + prof_samples = prof_samples + samples + local key_stack, key_stack2, key_state + -- Collect keys for sample. + if prof_states then + if prof_states == "v" then + key_state = map_vmmode[vmmode] or vmmode + else + key_state = zone:get() or "(none)" + end + end + if prof_fmt then + key_stack = profile.dumpstack(th, prof_fmt, prof_depth) + key_stack = key_stack:gsub("%[builtin#(%d+)%]", function(x) + return vmdef.ffnames[tonumber(x)] + end) + if prof_split == 2 then + local k1, k2 = key_stack:match("(.-) [<>] (.*)") + if k2 then key_stack, key_stack2 = k1, k2 end + elseif prof_split == 3 then + key_stack2 = profile.dumpstack(th, "l", 1) + end + end + -- Order keys. + local k1, k2 + if prof_split == 1 then + if key_state then + k1 = key_state + if key_stack then k2 = key_stack end + end + elseif key_stack then + k1 = key_stack + if key_stack2 then k2 = key_stack2 elseif key_state then k2 = key_state end + end + -- Coalesce samples in one or two levels. + if k1 then + local t1 = prof_count1 + t1[k1] = (t1[k1] or 0) + samples + if k2 then + local t2 = prof_count2 + local t3 = t2[k1] + if not t3 then t3 = {}; t2[k1] = t3 end + t3[k2] = (t3[k2] or 0) + samples + end + end +end + +------------------------------------------------------------------------------ + +-- Show top N list. +local function prof_top(count1, count2, samples, indent) + local t, n = {}, 0 + for k in pairs(count1) do + n = n + 1 + t[n] = k + end + sort(t, function(a, b) return count1[a] > count1[b] end) + for i=1,n do + local k = t[i] + local v = count1[k] + local pct = floor(v*100/samples + 0.5) + if pct < prof_min then break end + if not prof_raw then + out:write(format("%s%2d%% %s\n", indent, pct, k)) + elseif prof_raw == "r" then + out:write(format("%s%5d %s\n", indent, v, k)) + else + out:write(format("%s %d\n", k, v)) + end + if count2 then + local r = count2[k] + if r then + prof_top(r, nil, v, (prof_split == 3 or prof_split == 1) and " -- " or + (prof_depth < 0 and " -> " or " <- ")) + end + end + end +end + +-- Annotate source code +local function prof_annotate(count1, samples) + local files = {} + local ms = 0 + for k, v in pairs(count1) do + local pct = floor(v*100/samples + 0.5) + ms = math.max(ms, v) + if pct >= prof_min then + local file, line = k:match("^(.*):(%d+)$") + if not file then file = k; line = 0 end + local fl = files[file] + if not fl then fl = {}; files[file] = fl; files[#files+1] = file end + line = tonumber(line) + fl[line] = prof_raw and v or pct + end + end + sort(files) + local fmtv, fmtn = " %3d%% | %s\n", " | %s\n" + if prof_raw then + local n = math.max(5, math.ceil(math.log10(ms))) + fmtv = "%"..n.."d | %s\n" + fmtn = (" "):rep(n).." | %s\n" + end + local ann = prof_ann + for _, file in ipairs(files) do + local f0 = file:byte() + if f0 == 40 or f0 == 91 then + out:write(format("\n====== %s ======\n[Cannot annotate non-file]\n", file)) + break + end + local fp, err = io.open(file) + if not fp then + out:write(format("====== ERROR: %s: %s\n", file, err)) + break + end + out:write(format("\n====== %s ======\n", file)) + local fl = files[file] + local n, show = 1, false + if ann ~= 0 then + for i=1,ann do + if fl[i] then show = true; out:write("@@ 1 @@\n"); break end + end + end + for line in fp:lines() do + if line:byte() == 27 then + out:write("[Cannot annotate bytecode file]\n") + break + end + local v = fl[n] + if ann ~= 0 then + local v2 = fl[n+ann] + if show then + if v2 then show = n+ann elseif v then show = n + elseif show+ann < n then show = false end + elseif v2 then + show = n+ann + out:write(format("@@ %d @@\n", n)) + end + if not show then goto next end + end + if v then + out:write(format(fmtv, v, line)) + else + out:write(format(fmtn, line)) + end + ::next:: + n = n + 1 + end + fp:close() + end +end + +------------------------------------------------------------------------------ + +-- Finish profiling and dump result. +local function prof_finish() + if prof_ud then + profile.stop() + local samples = prof_samples + if samples == 0 then + if prof_raw ~= true then out:write("[No samples collected]\n") end + return + end + if prof_ann then + prof_annotate(prof_count1, samples) + else + prof_top(prof_count1, prof_count2, samples, "") + end + prof_count1 = nil + prof_count2 = nil + prof_ud = nil + if out ~= stdout then out:close() end + end +end + +-- Start profiling. +local function prof_start(mode) + local interval = "" + mode = mode:gsub("i%d*", function(s) interval = s; return "" end) + prof_min = 3 + mode = mode:gsub("m(%d+)", function(s) prof_min = tonumber(s); return "" end) + prof_depth = 1 + mode = mode:gsub("%-?%d+", function(s) prof_depth = tonumber(s); return "" end) + local m = {} + for c in mode:gmatch(".") do m[c] = c end + prof_states = m.z or m.v + if prof_states == "z" then zone = require("jit.zone") end + local scope = m.l or m.f or m.F or (prof_states and "" or "f") + local flags = (m.p or "") + prof_raw = m.r + if m.s then + prof_split = 2 + if prof_depth == -1 or m["-"] then prof_depth = -2 + elseif prof_depth == 1 then prof_depth = 2 end + elseif mode:find("[fF].*l") then + scope = "l" + prof_split = 3 + else + prof_split = (scope == "" or mode:find("[zv].*[lfF]")) and 1 or 0 + end + prof_ann = m.A and 0 or (m.a and 3) + if prof_ann then + scope = "l" + prof_fmt = "pl" + prof_split = 0 + prof_depth = 1 + elseif m.G and scope ~= "" then + prof_fmt = flags..scope.."Z;" + prof_depth = -100 + prof_raw = true + prof_min = 0 + elseif scope == "" then + prof_fmt = false + else + local sc = prof_split == 3 and m.f or m.F or scope + prof_fmt = flags..sc..(prof_depth >= 0 and "Z < " or "Z > ") + end + prof_count1 = {} + prof_count2 = {} + prof_samples = 0 + profile.start(scope:lower()..interval, prof_cb) + prof_ud = newproxy(true) + getmetatable(prof_ud).__gc = prof_finish +end + +------------------------------------------------------------------------------ + +local function start(mode, outfile) + if not outfile then outfile = os.getenv("LUAJIT_PROFILEFILE") end + if outfile then + out = outfile == "-" and stdout or assert(io.open(outfile, "w")) + else + out = stdout + end + prof_start(mode or "f") +end + +-- Public module functions. +return { + start = start, -- For -j command line option. + stop = prof_finish +} + diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/reload.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/reload.lua new file mode 100644 index 0000000..6189522 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/reload.lua @@ -0,0 +1,36 @@ +local reload = {} + +reload.reload_module = function(module_name, starts_with_only) + -- Default to starts with only + if starts_with_only == nil then + starts_with_only = true + end + + -- TODO: Might need to handle cpath / compiled lua packages? Not sure. + local matcher + if not starts_with_only then + matcher = function(pack) + return string.find(pack, module_name, 1, true) + end + else + local module_name_pattern = vim.pesc(module_name) + matcher = function(pack) + return string.find(pack, "^" .. module_name_pattern) + end + end + + -- Handle impatient.nvim automatically. + local luacache = (_G.__luacache or {}).cache + + for pack, _ in pairs(package.loaded) do + if matcher(pack) then + package.loaded[pack] = nil + + if luacache then + luacache[pack] = nil + end + end + end +end + +return reload diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/run.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/run.lua new file mode 100644 index 0000000..0661fc6 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/run.lua @@ -0,0 +1,28 @@ +local floatwin = require "plenary.window.float" + +local run = {} + +run.with_displayed_output = function(title_text, cmd, opts) + local views = floatwin.centered_with_top_win(title_text) + + local job_id = vim.fn.termopen(cmd) + + local count = 0 + while not vim.wait(1000, function() + return vim.fn.jobwait({ job_id }, 0)[1] == -1 + end) do + vim.cmd [[normal! G]] + count = count + 1 + + if count == 10 then + break + end + end + + vim.fn.win_gotoid(views.win_id) + vim.cmd [[startinsert]] + + return views.bufnr, views.win_id +end + +return run diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/scandir.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/scandir.lua new file mode 100644 index 0000000..d5b60ee --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/scandir.lua @@ -0,0 +1,620 @@ +local Path = require "plenary.path" +local os_sep = Path.path.sep +local F = require "plenary.functional" +local compat = require "plenary.compat" + +local uv = vim.loop + +local m = {} + +local make_gitignore = function(basepath) + local patterns = {} + local valid = false + for _, v in ipairs(basepath) do + local p = Path:new(v .. os_sep .. ".gitignore") + if p:exists() then + valid = true + patterns[v] = { ignored = {}, negated = {} } + for l in p:iter() do + local prefix = l:sub(1, 1) + local negated = prefix == "!" + if negated then + l = l:sub(2) + prefix = l:sub(1, 1) + end + if prefix == "/" then + l = v .. l + end + if not (prefix == "" or prefix == "#") then + local el = vim.trim(l) + el = el:gsub("%-", "%%-") + el = el:gsub("%.", "%%.") + el = el:gsub("/%*%*/", "/%%w+/") + el = el:gsub("%*%*", "") + el = el:gsub("%*", "%%w+") + el = el:gsub("%?", "%%w") + if el ~= "" then + table.insert(negated and patterns[v].negated or patterns[v].ignored, el) + end + end + end + end + end + if not valid then + return nil + end + return function(bp, entry) + for _, v in ipairs(bp) do + if entry:find(v, 1, true) then + local negated = false + for _, w in ipairs(patterns[v].ignored) do + if not negated and entry:match(w) then + for _, inverse in ipairs(patterns[v].negated) do + if not negated and entry:match(inverse) then + negated = true + end + end + if not negated then + return false + end + end + end + end + end + return true + end +end +-- exposed for testing +m.__make_gitignore = make_gitignore + +local handle_depth = function(base_paths, entry, depth) + for _, v in ipairs(base_paths) do + if entry:find(v, 1, true) then + local cut = entry:sub(#v + 1, -1) + cut = cut:sub(1, 1) == os_sep and cut:sub(2, -1) or cut + local _, count = cut:gsub(os_sep, "") + if depth <= (count + 1) then + return nil + end + end + end + return entry +end + +local gen_search_pat = function(pattern) + if type(pattern) == "string" then + return function(entry) + return entry:match(pattern) + end + elseif type(pattern) == "table" then + return function(entry) + for _, v in ipairs(pattern) do + if entry:match(v) then + return true + end + end + return false + end + elseif type(pattern) == "function" then + return pattern + end +end + +local process_item = function(opts, name, typ, current_dir, next_dir, bp, data, giti, msp) + if opts.hidden or name:sub(1, 1) ~= "." then + if typ == "directory" then + local entry = current_dir .. os_sep .. name + if opts.depth then + table.insert(next_dir, handle_depth(bp, entry, opts.depth)) + else + table.insert(next_dir, entry) + end + if opts.add_dirs or opts.only_dirs then + if not giti or giti(bp, entry .. "/") then + if not msp or msp(entry) then + table.insert(data, entry) + if opts.on_insert then + opts.on_insert(entry, typ) + end + end + end + end + elseif not opts.only_dirs then + local entry = current_dir .. os_sep .. name + if not giti or giti(bp, entry) then + if not msp or msp(entry) then + table.insert(data, entry) + if opts.on_insert then + opts.on_insert(entry, typ) + end + end + end + end + end +end + +--- m.scan_dir +-- Search directory recursive and syncronous +-- @param path: string or table +-- string has to be a valid path +-- table has to be a array of valid paths +-- @param opts: table to change behavior +-- opts.hidden (bool): if true hidden files will be added +-- opts.add_dirs (bool): if true dirs will also be added to the results +-- opts.only_dirs (bool): if true only dirs will be added to the results +-- opts.respect_gitignore (bool): if true will only add files that are not ignored by the git +-- opts.depth (int): depth on how deep the search should go +-- opts.search_pattern (regex): regex for which files will be added, string, table of strings, or fn(e) -> bool +-- opts.on_insert(entry): Will be called for each element +-- opts.silent (bool): if true will not echo messages that are not accessible +-- @return array with files +m.scan_dir = function(path, opts) + opts = opts or {} + + local data = {} + local base_paths = compat.flatten { path } + local next_dir = compat.flatten { path } + + local gitignore = opts.respect_gitignore and make_gitignore(base_paths) or nil + local match_search_pat = opts.search_pattern and gen_search_pat(opts.search_pattern) or nil + + for i = #base_paths, 1, -1 do + if uv.fs_access(base_paths[i], "X") == false then + if not F.if_nil(opts.silent, false, opts.silent) then + print(string.format("%s is not accessible by the current user!", base_paths[i])) + end + table.remove(base_paths, i) + end + end + if #base_paths == 0 then + return {} + end + + repeat + local current_dir = table.remove(next_dir, 1) + local fd = uv.fs_scandir(current_dir) + if fd then + while true do + local name, typ = uv.fs_scandir_next(fd) + if name == nil then + break + end + process_item(opts, name, typ, current_dir, next_dir, base_paths, data, gitignore, match_search_pat) + end + end + until #next_dir == 0 + return data +end + +--- m.scan_dir_async +-- Search directory recursive and asyncronous +-- @param path: string or table +-- string has to be a valid path +-- table has to be a array of valid paths +-- @param opts: table to change behavior +-- opts.hidden (bool): if true hidden files will be added +-- opts.add_dirs (bool): if true dirs will also be added to the results +-- opts.only_dirs (bool): if true only dirs will be added to the results +-- opts.respect_gitignore (bool): if true will only add files that are not ignored by git +-- opts.depth (int): depth on how deep the search should go +-- opts.search_pattern (regex): regex for which files will be added, string, table of strings, or fn(e) -> bool +-- opts.on_insert function(entry): will be called for each element +-- opts.on_exit function(results): will be called at the end +-- opts.silent (bool): if true will not echo messages that are not accessible +m.scan_dir_async = function(path, opts) + opts = opts or {} + + local data = {} + local base_paths = compat.flatten { path } + local next_dir = compat.flatten { path } + local current_dir = table.remove(next_dir, 1) + + -- TODO(conni2461): get gitignore is not async + local gitignore = opts.respect_gitignore and make_gitignore(base_paths) or nil + local match_search_pat = opts.search_pattern and gen_search_pat(opts.search_pattern) or nil + + -- TODO(conni2461): is not async. Shouldn't be that big of a problem but still + -- Maybe obers async pr can take me out of callback hell + for i = #base_paths, 1, -1 do + if uv.fs_access(base_paths[i], "X") == false then + if not F.if_nil(opts.silent, false, opts.silent) then + print(string.format("%s is not accessible by the current user!", base_paths[i])) + end + table.remove(base_paths, i) + end + end + if #base_paths == 0 then + return {} + end + + local read_dir + read_dir = function(err, fd) + if not err then + while true do + local name, typ = uv.fs_scandir_next(fd) + if name == nil then + break + end + process_item(opts, name, typ, current_dir, next_dir, base_paths, data, gitignore, match_search_pat) + end + if #next_dir == 0 then + if opts.on_exit then + opts.on_exit(data) + end + else + current_dir = table.remove(next_dir, 1) + uv.fs_scandir(current_dir, read_dir) + end + end + end + uv.fs_scandir(current_dir, read_dir) +end + +local gen_permissions = (function() + local conv_to_octal = function(nr) + local octal, i = 0, 1 + + while nr ~= 0 do + octal = octal + (nr % 8) * i + nr = math.floor(nr / 8) + i = i * 10 + end + + return octal + end + + local type_tbl = { [1] = "p", [2] = "c", [4] = "d", [6] = "b", [10] = ".", [12] = "l", [14] = "s" } + local permissions_tbl = { [0] = "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx" } + local bit_tbl = { 4, 2, 1 } + + return function(cache, mode) + if cache[mode] then + return cache[mode] + end + + local octal = string.format("%6d", conv_to_octal(mode)) + local l4 = octal:sub(#octal - 3, -1) + local bit = tonumber(l4:sub(1, 1)) + + local result = type_tbl[tonumber(octal:sub(1, 2))] or "-" + for i = 2, #l4 do + result = result .. permissions_tbl[tonumber(l4:sub(i, i))] + if bit - bit_tbl[i - 1] >= 0 then + result = result:sub(1, -2) .. (bit_tbl[i - 1] == 1 and "T" or "S") + bit = bit - bit_tbl[i - 1] + end + end + + cache[mode] = result + return result + end +end)() + +local gen_size = (function() + local size_types = { "", "K", "M", "G", "T", "P", "E", "Z" } + + return function(size) + -- TODO(conni2461): If type directory we could just return 4.0K + for _, v in ipairs(size_types) do + if math.abs(size) < 1024.0 then + if math.abs(size) > 9 then + return string.format("%3d%s", size, v) + else + return string.format("%3.1f%s", size, v) + end + end + size = size / 1024.0 + end + return string.format("%.1f%s", size, "Y") + end +end)() + +local gen_date = (function() + local current_year = os.date "%Y" + return function(mtime) + if current_year ~= os.date("%Y", mtime) then + return os.date("%b %d %Y", mtime) + end + return os.date("%b %d %H:%M", mtime) + end +end)() + +local get_username = (function() + local fallback = function(tbl, id) + if not tbl then + return id + end + if tbl[id] then + return tbl[id] + end + tbl[id] = tostring(id) + return id + end + + if jit and os_sep ~= "\\" then + local ffi = require "ffi" + ffi.cdef [[ + typedef unsigned int __uid_t; + typedef __uid_t uid_t; + typedef unsigned int __gid_t; + typedef __gid_t gid_t; + + typedef struct { + char *pw_name; + char *pw_passwd; + __uid_t pw_uid; + __gid_t pw_gid; + char *pw_gecos; + char *pw_dir; + char *pw_shell; + } passwd; + + passwd *getpwuid(uid_t uid); + ]] + + local ffi_func = function(tbl, id) + if tbl[id] then + return tbl[id] + end + local struct = ffi.C.getpwuid(id) + local name + if struct == nil then + name = tostring(id) + else + name = ffi.string(struct.pw_name) + end + tbl[id] = name + return name + end + + local ok = pcall(ffi_func, {}, 1000) + if ok then + return ffi_func + else + return fallback + end + else + return fallback + end +end)() + +local get_groupname = (function() + local fallback = function(tbl, id) + if not tbl then + return id + end + if tbl[id] then + return tbl[id] + end + tbl[id] = tostring(id) + return id + end + + if jit and os_sep ~= "\\" then + local ffi = require "ffi" + ffi.cdef [[ + typedef unsigned int __gid_t; + typedef __gid_t gid_t; + + typedef struct { + char *gr_name; + char *gr_passwd; + __gid_t gr_gid; + char **gr_mem; + } group; + group *getgrgid(gid_t gid); + ]] + + local ffi_func = function(tbl, id) + if tbl[id] then + return tbl[id] + end + local struct = ffi.C.getgrgid(id) + local name + if struct == nil then + name = tostring(id) + else + name = ffi.string(struct.gr_name) + end + tbl[id] = name + return name + end + local ok = pcall(ffi_func, {}, 1000) + if ok then + return ffi_func + else + return fallback + end + else + return fallback + end +end)() + +local get_max_len = function(tbl) + if not tbl then + return 0 + end + local max_len = 0 + for _, v in pairs(tbl) do + if #v > max_len then + max_len = #v + end + end + return max_len +end + +local gen_ls = function(data, path, opts) + if not data or #data == 0 then + return {}, {} + end + + local check_link = function(per, file) + if per:sub(1, 1) == "l" then + local resolved = uv.fs_realpath(path .. os_sep .. file) + if not resolved then + return file + end + if resolved:sub(1, #path) == path then + resolved = resolved:sub(#path + 2, -1) + end + return string.format("%s -> %s", file, resolved) + end + return file + end + + local results, sections = {}, {} + + local users_tbl = os_sep ~= "\\" and {} or nil + local groups_tbl = os_sep ~= "\\" and {} or nil + + local stats, permissions_cache = {}, {} + for _, v in ipairs(data) do + local stat = uv.fs_lstat(v) + if stat then + stats[v] = stat + get_username(users_tbl, stat.uid) + get_groupname(groups_tbl, stat.gid) + end + end + + local insert_in_results = (function() + if not users_tbl and not groups_tbl then + local section_spacing_tbl = { [5] = 2, [6] = 0 } + + return function(...) + local args = { ... } + local section = { + { start_index = 01, end_index = 11 }, -- permissions, hardcoded indexes + { start_index = 12, end_index = 17 }, -- size, hardcoded indexes + } + local cur_index = 19 + for k = 5, 6 do + local v = section_spacing_tbl[k] + local end_index = cur_index + #args[k] + table.insert(section, { start_index = cur_index, end_index = end_index }) + cur_index = end_index + v + end + table.insert(sections, section) + table.insert( + results, + string.format("%10s %5s %s %s", args[1], args[2], args[5], check_link(args[1], args[6])) + ) + end + else + local max_user_len = get_max_len(users_tbl) + local max_group_len = get_max_len(groups_tbl) + + local section_spacing_tbl = { + [3] = { max = max_user_len, add = 1 }, + [4] = { max = max_group_len, add = 2 }, + [5] = { add = 2 }, + [6] = { add = 0 }, + } + local fmt_str = "%10s %5s %-" .. max_user_len .. "s %-" .. max_group_len .. "s %s %s" + + return function(...) + local args = { ... } + local section = { + { start_index = 01, end_index = 11 }, -- permissions, hardcoded indexes + { start_index = 12, end_index = 17 }, -- size, hardcoded indexes + } + local cur_index = 18 + for k = 3, 6 do + local v = section_spacing_tbl[k] + local end_index = cur_index + #args[k] + table.insert(section, { start_index = cur_index, end_index = end_index }) + if v.max then + cur_index = cur_index + v.max + v.add + else + cur_index = end_index + v.add + end + end + table.insert(sections, section) + table.insert( + results, + string.format(fmt_str, args[1], args[2], args[3], args[4], args[5], check_link(args[1], args[6])) + ) + end + end + end)() + + for name, stat in pairs(stats) do + insert_in_results( + gen_permissions(permissions_cache, stat.mode), + gen_size(stat.size), + get_username(users_tbl, stat.uid), + get_groupname(groups_tbl, stat.gid), + gen_date(stat.mtime.sec), + name:sub(#path + 2, -1) + ) + end + + if opts and opts.group_directories_first then + local sorted_results = {} + local sorted_sections = {} + for k, v in ipairs(results) do + if v:sub(1, 1) == "d" then + table.insert(sorted_results, v) + table.insert(sorted_sections, sections[k]) + end + end + for k, v in ipairs(results) do + if v:sub(1, 1) ~= "d" then + table.insert(sorted_results, v) + table.insert(sorted_sections, sections[k]) + end + end + return sorted_results, sorted_sections + else + return results, sections + end +end + +--- m.ls +-- List directory contents. Will always apply --long option. Use scan_dir for without --long +-- @param path: string +-- string has to be a valid path +-- @param opts: table to change behavior +-- opts.hidden (bool): if true hidden files will be added +-- opts.add_dirs (bool): if true dirs will also be added to the results, default: true +-- opts.respect_gitignore (bool): if true will only add files that are not ignored by git +-- opts.depth (int): depth on how deep the search should go, default: 1 +-- opts.group_directories_first (bool): same as real ls +-- @return array with formatted output +m.ls = function(path, opts) + opts = opts or {} + opts.depth = opts.depth or 1 + opts.add_dirs = opts.add_dirs or true + local data = m.scan_dir(path, opts) + + return gen_ls(data, path, opts) +end + +--- m.ls_async +-- List directory contents. Will always apply --long option. Use scan_dir for without --long +-- @param path: string +-- string has to be a valid path +-- @param opts: table to change behavior +-- opts.hidden (bool): if true hidden files will be added +-- opts.add_dirs (bool): if true dirs will also be added to the results, default: true +-- opts.respect_gitignore (bool): if true will only add files that are not ignored by git +-- opts.depth (int): depth on how deep the search should go, default: 1 +-- opts.group_directories_first (bool): same as real ls +-- opts.on_exit function(results): will be called at the end (required) +m.ls_async = function(path, opts) + opts = opts or {} + opts.depth = opts.depth or 1 + opts.add_dirs = opts.add_dirs or true + + local opts_copy = vim.deepcopy(opts) + + opts_copy.on_exit = function(data) + if opts.on_exit then + opts.on_exit(gen_ls(data, path, opts_copy)) + end + end + + m.scan_dir_async(path, opts_copy) +end + +return m diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/strings.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/strings.lua new file mode 100644 index 0000000..3e8e5bc --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/strings.lua @@ -0,0 +1,204 @@ +local path = require("plenary.path").path + +local M = {} + +M.strdisplaywidth = (function() + local fallback = function(str, col) + str = tostring(str) + if vim.in_fast_event() then + return #str - (col or 0) + end + return vim.fn.strdisplaywidth(str, col) + end + + if jit and path.sep ~= [[\]] then + local ffi = require "ffi" + ffi.cdef [[ + typedef unsigned char char_u; + int linetabsize_col(int startcol, char_u *s); + ]] + + local ffi_func = function(str, col) + str = tostring(str) + local startcol = col or 0 + local s = ffi.new("char[?]", #str + 1) + ffi.copy(s, str) + return ffi.C.linetabsize_col(startcol, s) - startcol + end + + local ok = pcall(ffi_func, "hello") + if ok then + return ffi_func + else + return fallback + end + else + return fallback + end +end)() + +M.strcharpart = (function() + local fallback = function(str, nchar, charlen) + if vim.in_fast_event() then + return str:sub(nchar + 1, charlen) + end + return vim.fn.strcharpart(str, nchar, charlen) + end + + if jit and path.sep ~= [[\]] then + local ffi = require "ffi" + ffi.cdef [[ + typedef unsigned char char_u; + int utf_ptr2len(const char_u *const p); + ]] + + local function utf_ptr2len(str) + local c_str = ffi.new("char[?]", #str + 1) + ffi.copy(c_str, str) + return ffi.C.utf_ptr2len(c_str) + end + + local ok = pcall(utf_ptr2len, "🔭") + if not ok then + return fallback + end + + return function(str, nchar, charlen) + local nbyte = 0 + if nchar > 0 then + while nchar > 0 and nbyte < #str do + nbyte = nbyte + utf_ptr2len(str:sub(nbyte + 1)) + nchar = nchar - 1 + end + else + nbyte = nchar + end + + local len = 0 + if charlen then + while charlen > 0 and nbyte + len < #str do + local off = nbyte + len + if off < 0 then + len = len + 1 + else + len = len + utf_ptr2len(str:sub(off + 1)) + end + charlen = charlen - 1 + end + else + len = #str - nbyte + end + + if nbyte < 0 then + len = len + nbyte + nbyte = 0 + elseif nbyte > #str then + nbyte = #str + end + if len < 0 then + len = 0 + elseif nbyte + len > #str then + len = #str - nbyte + end + + return str:sub(nbyte + 1, nbyte + len) + end + else + return fallback + end +end)() + +local truncate = function(str, len, dots, direction) + if M.strdisplaywidth(str) <= len then + return str + end + local start = direction > 0 and 0 or str:len() + local current = 0 + local result = "" + local len_of_dots = M.strdisplaywidth(dots) + local concat = function(a, b, dir) + if dir > 0 then + return a .. b + else + return b .. a + end + end + while true do + local part = M.strcharpart(str, start, 1) + current = current + M.strdisplaywidth(part) + if (current + len_of_dots) > len then + result = concat(result, dots, direction) + break + end + result = concat(result, part, direction) + start = start + direction + end + return result +end + +M.truncate = function(str, len, dots, direction) + str = tostring(str) -- We need to make sure its an actually a string and not a number + dots = dots or "…" + direction = direction or 1 + if direction ~= 0 then + return truncate(str, len, dots, direction) + else + if M.strdisplaywidth(str) <= len then + return str + end + local len1 = math.floor((len + M.strdisplaywidth(dots)) / 2) + local s1 = truncate(str, len1, dots, 1) + local len2 = len - M.strdisplaywidth(s1) + M.strdisplaywidth(dots) + local s2 = truncate(str, len2, dots, -1) + return s1 .. s2:sub(dots:len() + 1) + end +end + +M.align_str = function(string, width, right_justify) + local str_len = M.strdisplaywidth(string) + return right_justify and string.rep(" ", width - str_len) .. string or string .. string.rep(" ", width - str_len) +end + +M.dedent = function(str, leave_indent) + -- Check each line and detect the minimum indent. + local indent + local info = {} + for line in str:gmatch "[^\n]*\n?" do + -- It matches '' for the last line. + if line ~= "" then + local chars, width + local line_indent = line:match "^[ \t]+" + if line_indent then + chars = #line_indent + width = M.strdisplaywidth(line_indent) + if not indent or width < indent then + indent = width + end + -- Ignore empty lines + elseif line ~= "\n" then + indent = 0 + end + table.insert(info, { line = line, chars = chars, width = width }) + end + end + + -- Build up the result + leave_indent = leave_indent or 0 + local result = {} + for _, i in ipairs(info) do + local line + if i.chars then + local content = i.line:sub(i.chars + 1) + local indent_width = i.width - indent + leave_indent + line = (" "):rep(indent_width) .. content + elseif i.line == "\n" then + line = "\n" + else + line = (" "):rep(leave_indent) .. i.line + end + table.insert(result, line) + end + return table.concat(result) +end + +return M diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/tbl.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/tbl.lua new file mode 100644 index 0000000..276e9ab --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/tbl.lua @@ -0,0 +1,40 @@ +local tbl = {} + +function tbl.apply_defaults(original, defaults) + if original == nil then + original = {} + end + + original = vim.deepcopy(original) + + for k, v in pairs(defaults) do + if original[k] == nil then + original[k] = v + end + end + + return original +end + +function tbl.pack(...) + return { n = select("#", ...), ... } +end + +function tbl.unpack(t, i, j) + return unpack(t, i or 1, j or t.n or #t) +end + +---Freeze a table. A frozen table is not able to be modified. +---http://lua-users.org/wiki/ReadOnlyTables +---@param t table +---@return table +function tbl.freeze(t) + return setmetatable({}, { + __index = t, + __newindex = function() + error "Attempt to modify frozen table" + end, + }) +end + +return tbl diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/test_harness.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/test_harness.lua new file mode 100644 index 0000000..a548db7 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/test_harness.lua @@ -0,0 +1,246 @@ +local Path = require "plenary.path" +local Job = require "plenary.job" + +local f = require "plenary.functional" +local log = require "plenary.log" +local win_float = require "plenary.window.float" + +local headless = require("plenary.nvim_meta").is_headless + +local plenary_dir = vim.fn.fnamemodify(debug.getinfo(1).source:match "@?(.*[/\\])", ":p:h:h:h") + +local harness = {} + +local print_output = vim.schedule_wrap(function(_, ...) + for _, v in ipairs { ... } do + io.stdout:write(tostring(v)) + io.stdout:write "\n" + end + + vim.cmd [[mode]] +end) + +local get_nvim_output = function(job_id) + return vim.schedule_wrap(function(bufnr, ...) + if not vim.api.nvim_buf_is_valid(bufnr) then + return + end + for _, v in ipairs { ... } do + vim.api.nvim_chan_send(job_id, v .. "\r\n") + end + end) +end + +function harness.test_directory_command(command) + local split_string = vim.split(command, " ") + local directory = vim.fn.expand(table.remove(split_string, 1)) + + local opts = assert(loadstring("return " .. table.concat(split_string, " ")))() + + return harness.test_directory(directory, opts) +end + +local function test_paths(paths, opts) + local minimal = not opts or not opts.init or opts.minimal or opts.minimal_init + + opts = vim.tbl_deep_extend("force", { + nvim_cmd = vim.v.progpath, + winopts = { winblend = 3 }, + sequential = false, + keep_going = true, + timeout = 50000, + }, opts or {}) + + vim.env.PLENARY_TEST_TIMEOUT = opts.timeout + + local res = {} + if not headless then + res = win_float.percentage_range_window(0.95, 0.70, opts.winopts) + + res.job_id = vim.api.nvim_open_term(res.bufnr, {}) + vim.api.nvim_buf_set_keymap(res.bufnr, "n", "q", ":q", {}) + + vim.api.nvim_win_set_option(res.win_id, "winhl", "Normal:Normal") + vim.api.nvim_win_set_option(res.win_id, "conceallevel", 3) + vim.api.nvim_win_set_option(res.win_id, "concealcursor", "n") + + if res.border_win_id then + vim.api.nvim_win_set_option(res.border_win_id, "winhl", "Normal:Normal") + end + + if res.bufnr then + vim.api.nvim_buf_set_option(res.bufnr, "filetype", "PlenaryTestPopup") + end + vim.cmd "mode" + end + + local outputter = headless and print_output or get_nvim_output(res.job_id) + + local path_len = #paths + local failure = false + + local jobs = vim.tbl_map(function(p) + local args = { + "--headless", + "-c", + "set rtp+=.," .. vim.fn.escape(plenary_dir, " ") .. " | runtime plugin/plenary.vim", + } + + if minimal then + table.insert(args, "--noplugin") + if opts.minimal_init then + table.insert(args, "-u") + table.insert(args, opts.minimal_init) + end + elseif opts.init ~= nil then + table.insert(args, "-u") + table.insert(args, opts.init) + end + + table.insert(args, "-c") + table.insert(args, string.format('lua require("plenary.busted").run("%s")', p:absolute():gsub("\\", "\\\\"))) + + local job = Job:new { + command = opts.nvim_cmd, + args = args, + + -- Can be turned on to debug + on_stdout = function(_, data) + if path_len == 1 then + outputter(res.bufnr, data) + end + end, + + on_stderr = function(_, data) + if path_len == 1 then + outputter(res.bufnr, data) + end + end, + + on_exit = vim.schedule_wrap(function(j_self, _, _) + if path_len ~= 1 then + outputter(res.bufnr, unpack(j_self:stderr_result())) + outputter(res.bufnr, unpack(j_self:result())) + end + + vim.cmd "mode" + end), + } + job.nvim_busted_path = p.filename + return job + end, paths) + + log.debug "Running..." + for i, j in ipairs(jobs) do + outputter(res.bufnr, "Scheduling: " .. j.nvim_busted_path) + j:start() + if opts.sequential then + log.debug("... Sequential wait for job number", i) + if not Job.join(j, opts.timeout) then + log.debug("... Timed out job number", i) + failure = true + pcall(function() + j.handle:kill(15) -- SIGTERM + end) + else + log.debug("... Completed job number", i, j.code, j.signal) + failure = failure or j.code ~= 0 or j.signal ~= 0 + end + if failure and not opts.keep_going then + break + end + end + end + + -- TODO: Probably want to let people know when we've completed everything. + if not headless then + return + end + + if not opts.sequential then + table.insert(jobs, opts.timeout) + log.debug "... Parallel wait" + Job.join(unpack(jobs)) + log.debug "... Completed jobs" + table.remove(jobs, table.getn(jobs)) + failure = f.any(function(_, v) + return v.code ~= 0 + end, jobs) + end + vim.wait(100) + + if headless then + if failure then + return vim.cmd "1cq" + end + + return vim.cmd "0cq" + end +end + +function harness.test_directory(directory, opts) + print "Starting..." + directory = directory:gsub("\\", "/") + local paths = harness._find_files_to_run(directory) + + -- Paths work strangely on Windows, so lets have abs paths + if vim.fn.has "win32" == 1 or vim.fn.has "win64" == 1 then + paths = vim.tbl_map(function(p) + return Path:new(directory, p.filename) + end, paths) + end + + test_paths(paths, opts) +end + +function harness.test_file(filepath) + test_paths { Path:new(filepath) } +end + +function harness._find_files_to_run(directory) + local finder + if vim.fn.has "win32" == 1 or vim.fn.has "win64" == 1 then + -- On windows use powershell Get-ChildItem instead + local cmd = vim.fn.executable "pwsh.exe" == 1 and "pwsh" or "powershell" + finder = Job:new { + command = cmd, + args = { "-NoProfile", "-Command", [[Get-ChildItem -Recurse -n -Filter "*_spec.lua"]] }, + cwd = directory, + } + else + -- everywhere else use find + finder = Job:new { + command = "find", + args = { directory, "-type", "f", "-name", "*_spec.lua" }, + } + end + + return vim.tbl_map(Path.new, finder:sync(vim.env.PLENARY_TEST_TIMEOUT)) +end + +function harness._run_path(test_type, directory) + local paths = harness._find_files_to_run(directory) + + local bufnr = 0 + local win_id = 0 + + for _, p in pairs(paths) do + print " " + print("Loading Tests For: ", p:absolute(), "\n") + + local ok, _ = pcall(function() + dofile(p:absolute()) + end) + + if not ok then + print "Failed to load file" + end + end + + harness:run(test_type, bufnr, win_id) + vim.cmd "qa!" + + return paths +end + +return harness diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/vararg/init.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/vararg/init.lua new file mode 100644 index 0000000..1398d60 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/vararg/init.lua @@ -0,0 +1,3 @@ +return { + rotate = require "plenary.vararg.rotate", +} diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/vararg/rotate.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/vararg/rotate.lua new file mode 100644 index 0000000..6b71c63 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/vararg/rotate.lua @@ -0,0 +1,83 @@ +---@brief [[ +---Do not edit this file, it was generated! +---Provides a function to rotate a lua vararg +---@brief ]] +local tbl = require "plenary.tbl" + +local rotate_lookup = {} + +rotate_lookup[1] = function(A0) + return A0 +end + +rotate_lookup[2] = function(A0, A1) + return A1, A0 +end + +rotate_lookup[3] = function(A0, A1, A2) + return A1, A2, A0 +end + +rotate_lookup[4] = function(A0, A1, A2, A3) + return A1, A2, A3, A0 +end + +rotate_lookup[5] = function(A0, A1, A2, A3, A4) + return A1, A2, A3, A4, A0 +end + +rotate_lookup[6] = function(A0, A1, A2, A3, A4, A5) + return A1, A2, A3, A4, A5, A0 +end + +rotate_lookup[7] = function(A0, A1, A2, A3, A4, A5, A6) + return A1, A2, A3, A4, A5, A6, A0 +end + +rotate_lookup[8] = function(A0, A1, A2, A3, A4, A5, A6, A7) + return A1, A2, A3, A4, A5, A6, A7, A0 +end + +rotate_lookup[9] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8) + return A1, A2, A3, A4, A5, A6, A7, A8, A0 +end + +rotate_lookup[10] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9) + return A1, A2, A3, A4, A5, A6, A7, A8, A9, A0 +end + +rotate_lookup[11] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10) + return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A0 +end + +rotate_lookup[12] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11) + return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A0 +end + +rotate_lookup[13] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12) + return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A0 +end + +rotate_lookup[14] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13) + return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A0 +end + +rotate_lookup[15] = function(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14) + return A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A0 +end + +local function rotate_n(first, ...) + local n = select("#", ...) + 1 + local args = tbl.pack(...) + args[n] = first + return tbl.unpack(args, 1, n) +end + +local function rotate(nargs, ...) + if nargs == nil or nargs < 1 then + return + end + return (rotate_lookup[nargs] or rotate_n)(...) +end + +return rotate diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/window/border.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/window/border.lua new file mode 100644 index 0000000..a9fe34f --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/window/border.lua @@ -0,0 +1,296 @@ +local strings = require "plenary.strings" + +local Border = {} + +Border.__index = Border + +Border._default_thickness = { + top = 1, + right = 1, + bot = 1, + left = 1, +} + +local calc_left_start = function(title_pos, title_len, total_width) + if string.find(title_pos, "W") then + return 0 + elseif string.find(title_pos, "E") then + return total_width - title_len + else + return math.floor((total_width - title_len) / 2) + end +end + +local create_horizontal_line = function(title, pos, width, left_char, mid_char, right_char) + local title_len + if title == "" then + title_len = 0 + else + local len = strings.strdisplaywidth(title) + local max_title_width = width - 2 + if len > max_title_width then + title = strings.truncate(title, max_title_width) + len = strings.strdisplaywidth(title) + end + title = string.format(" %s ", title) + title_len = len + 2 + end + + local left_start = calc_left_start(pos, title_len, width) + + local horizontal_line = string.format( + "%s%s%s%s%s", + left_char, + string.rep(mid_char, left_start), + title, + string.rep(mid_char, width - title_len - left_start), + right_char + ) + local ranges = {} + if title_len ~= 0 then + -- Need to calculate again due to multi-byte characters + local r_start = string.len(left_char) + math.max(left_start, 0) * string.len(mid_char) + ranges = { { r_start, r_start + string.len(title) } } + end + return horizontal_line, ranges +end + +function Border._create_lines(content_win_id, content_win_options, border_win_options) + local content_pos = vim.api.nvim_win_get_position(content_win_id) + local content_height = vim.api.nvim_win_get_height(content_win_id) + local content_width = vim.api.nvim_win_get_width(content_win_id) + + -- TODO: Handle border width, which I haven't right here. + local thickness = border_win_options.border_thickness + + local top_enabled = thickness.top == 1 + local right_enabled = thickness.right == 1 and content_pos[2] + content_width < vim.o.columns + local bot_enabled = thickness.bot == 1 + local left_enabled = thickness.left == 1 and content_pos[2] > 0 + + border_win_options.border_thickness.left = left_enabled and 1 or 0 + border_win_options.border_thickness.right = right_enabled and 1 or 0 + + local border_lines = {} + local ranges = {} + + -- border_win_options.title should have be a list with entries of the + -- form: { pos = foo, text = bar }. + -- pos can take values in { "NW", "N", "NE", "SW", "S", "SE" } + local titles = type(border_win_options.title) == "string" and { { pos = "N", text = border_win_options.title } } + or border_win_options.title + or {} + + local topline = nil + local topleft = (left_enabled and border_win_options.topleft) or "" + local topright = (right_enabled and border_win_options.topright) or "" + -- Only calculate the topline if there is space above the first content row (relative to the editor) + if content_pos[1] > 0 then + for _, title in ipairs(titles) do + if string.find(title.pos, "N") then + local top_ranges + topline, top_ranges = create_horizontal_line( + title.text, + title.pos, + content_win_options.width, + topleft, + border_win_options.top or "", + topright + ) + for _, r in pairs(top_ranges) do + table.insert(ranges, { 0, r[1], r[2] }) + end + break + end + end + if topline == nil then + if top_enabled then + topline = topleft .. string.rep(border_win_options.top, content_win_options.width) .. topright + end + end + else + border_win_options.border_thickness.top = 0 + end + + if topline then + table.insert(border_lines, topline) + end + + local middle_line = string.format( + "%s%s%s", + (left_enabled and border_win_options.left) or "", + string.rep(" ", content_win_options.width), + (right_enabled and border_win_options.right) or "" + ) + + for _ = 1, content_win_options.height do + table.insert(border_lines, middle_line) + end + + local botline = nil + local botleft = (left_enabled and border_win_options.botleft) or "" + local botright = (right_enabled and border_win_options.botright) or "" + if content_pos[1] + content_height < vim.o.lines then + for _, title in ipairs(titles) do + if string.find(title.pos, "S") then + local bot_ranges + botline, bot_ranges = create_horizontal_line( + title.text, + title.pos, + content_win_options.width, + botleft, + border_win_options.bot or "", + botright + ) + for _, r in pairs(bot_ranges) do + table.insert(ranges, { content_win_options.height + thickness.top, r[1], r[2] }) + end + break + end + end + if botline == nil then + if bot_enabled then + botline = botleft .. string.rep(border_win_options.bot, content_win_options.width) .. botright + end + end + else + border_win_options.border_thickness.bot = 0 + end + + if botline then + table.insert(border_lines, botline) + end + + return border_lines, ranges +end + +local set_title_highlights = function(bufnr, ranges, hl) + -- Check if both `hl` and `ranges` are provided, and `ranges` is not the empty table. + if hl and ranges and next(ranges) then + for _, r in pairs(ranges) do + vim.api.nvim_buf_add_highlight(bufnr, -1, hl, r[1], r[2], r[3]) + end + end +end + +function Border:change_title(new_title, pos) + if self._border_win_options.title == new_title then + return + end + + pos = pos + or (self._border_win_options.title and self._border_win_options.title[1] and self._border_win_options.title[1].pos) + if pos == nil then + self._border_win_options.title = new_title + else + self._border_win_options.title = { { text = new_title, pos = pos } } + end + + self.contents, self.title_ranges = + Border._create_lines(self.content_win_id, self.content_win_options, self._border_win_options) + vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents) + + set_title_highlights(self.bufnr, self.title_ranges, self._border_win_options.titlehighlight) +end + +-- Updates characters for border lines, and returns nvim_win_config +-- (generally used in conjunction with `move` or `new`) +function Border:__align_calc_config(content_win_options, border_win_options) + border_win_options = vim.tbl_deep_extend("keep", border_win_options, { + border_thickness = Border._default_thickness, + + -- Border options, could be passed as a list? + topleft = "╔", + topright = "╗", + top = "═", + left = "║", + right = "║", + botleft = "╚", + botright = "╝", + bot = "═", + }) + + -- Ensure the relevant contents and border win_options are set + self._border_win_options = border_win_options + self.content_win_options = content_win_options + -- Update border characters and title_ranges + self.contents, self.title_ranges = Border._create_lines(self.content_win_id, content_win_options, border_win_options) + + vim.api.nvim_buf_set_option(self.bufnr, "modifiable", true) + vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents) + + local thickness = border_win_options.border_thickness + local nvim_win_config = { + anchor = content_win_options.anchor, + relative = content_win_options.relative, + style = "minimal", + row = content_win_options.row - thickness.top, + col = content_win_options.col - thickness.left, + width = content_win_options.width + thickness.left + thickness.right, + height = content_win_options.height + thickness.top + thickness.bot, + zindex = content_win_options.zindex or 50, + noautocmd = content_win_options.noautocmd, + focusable = vim.F.if_nil(border_win_options.focusable, false), + border = "none", + } + + return nvim_win_config +end + +-- Sets the size and position of the given Border. +-- Can be used to create a new window (with `create_window = true`) +-- or change an existing one +function Border:move(content_win_options, border_win_options) + -- Update lines in border buffer, and get config for border window + local nvim_win_config = self:__align_calc_config(content_win_options, border_win_options) + + -- Set config for border window + vim.api.nvim_win_set_config(self.win_id, nvim_win_config) + + set_title_highlights(self.bufnr, self.title_ranges, self._border_win_options.titlehighlight) +end + +function Border:new(content_bufnr, content_win_id, content_win_options, border_win_options) + assert(type(content_win_id) == "number", "Must supply a valid win_id. It's possible you forgot to call with ':'") + + local obj = {} + + obj.content_win_id = content_win_id + + obj.bufnr = vim.api.nvim_create_buf(false, true) + assert(obj.bufnr, "Failed to create border buffer") + vim.api.nvim_buf_set_option(obj.bufnr, "bufhidden", "wipe") + + -- Create a border window and buffer, with border characters around the edge + local nvim_win_config = Border.__align_calc_config(obj, content_win_options, border_win_options) + obj.win_id = vim.api.nvim_open_win(obj.bufnr, false, nvim_win_config) + + if border_win_options.highlight then + vim.api.nvim_win_set_option(obj.win_id, "winhl", border_win_options.highlight) + end + + set_title_highlights(obj.bufnr, obj.title_ranges, obj._border_win_options.titlehighlight) + + vim.cmd( + string.format( + "autocmd BufDelete ++nested ++once :lua require('plenary.window').close_related_win(%s, %s)", + content_bufnr, + content_win_id, + obj.win_id + ) + ) + + vim.cmd( + string.format( + "autocmd WinClosed ++nested ++once :lua require('plenary.window').try_close(%s, true)", + content_bufnr, + obj.win_id + ) + ) + + setmetatable(obj, Border) + + return obj +end + +return Border diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/window/float.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/window/float.lua new file mode 100644 index 0000000..86e5c04 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/window/float.lua @@ -0,0 +1,212 @@ +local Border = require "plenary.window.border" +local tbl = require "plenary.tbl" + +_AssociatedBufs = {} + +local clear_buf_on_leave = function(bufnr) + vim.cmd( + string.format( + "autocmd WinLeave,BufLeave,BufDelete ++once ++nested lua require('plenary.window.float').clear(%s)", + bufnr, + bufnr + ) + ) +end + +local win_float = {} + +win_float.default_options = { + winblend = 15, + percentage = 0.9, +} + +function win_float.default_opts(options) + options = tbl.apply_defaults(options, win_float.default_options) + + local width = math.floor(vim.o.columns * options.percentage) + local height = math.floor(vim.o.lines * options.percentage) + + local top = math.floor(((vim.o.lines - height) / 2) - 1) + local left = math.floor((vim.o.columns - width) / 2) + + local opts = { + relative = "editor", + row = top, + col = left, + width = width, + height = height, + style = "minimal", + } + + return opts +end + +function win_float.centered(options) + options = tbl.apply_defaults(options, win_float.default_options) + + local win_opts = win_float.default_opts(options) + + local bufnr = options.bufnr or vim.api.nvim_create_buf(false, true) + local win_id = vim.api.nvim_open_win(bufnr, true, win_opts) + + vim.cmd "setlocal nocursorcolumn" + vim.api.nvim_win_set_option(win_id, "winblend", options.winblend) + + vim.cmd(string.format("autocmd WinLeave silent! execute 'bdelete! %s'", bufnr)) + + return { + bufnr = bufnr, + win_id = win_id, + } +end + +function win_float.centered_with_top_win(top_text, options) + options = tbl.apply_defaults(options, win_float.default_options) + + table.insert(top_text, 1, string.rep("=", 80)) + table.insert(top_text, string.rep("=", 80)) + + local primary_win_opts = win_float.default_opts(nil, nil, options) + local minor_win_opts = vim.deepcopy(primary_win_opts) + + primary_win_opts.height = primary_win_opts.height - #top_text - 1 + primary_win_opts.row = primary_win_opts.row + #top_text + 1 + + minor_win_opts.height = #top_text + + local minor_bufnr = vim.api.nvim_create_buf(false, true) + local minor_win_id = vim.api.nvim_open_win(minor_bufnr, true, minor_win_opts) + + vim.cmd "setlocal nocursorcolumn" + vim.api.nvim_win_set_option(minor_win_id, "winblend", options.winblend) + + vim.api.nvim_buf_set_lines(minor_bufnr, 0, -1, false, top_text) + + local primary_bufnr = vim.api.nvim_create_buf(false, true) + local primary_win_id = vim.api.nvim_open_win(primary_bufnr, true, primary_win_opts) + + vim.cmd "setlocal nocursorcolumn" + vim.api.nvim_win_set_option(primary_win_id, "winblend", options.winblend) + + -- vim.cmd( + -- string.format( + -- "autocmd WinLeave,BufDelete,BufLeave ++once ++nested silent! execute 'bdelete! %s'", + -- primary_buf, + -- minor_buf + -- ) + -- ) + + -- vim.cmd( + -- string.format( + -- "autocmd WinLeave,BufDelete,BufLeave ++once ++nested silent! execute 'bdelete! %s'", + -- primary_buf + -- ) + -- ) + + local primary_border = Border:new(primary_bufnr, primary_win_id, primary_win_opts, {}) + local minor_border = Border:new(minor_bufnr, minor_win_id, minor_win_opts, {}) + + _AssociatedBufs[primary_bufnr] = { + primary_win_id, + minor_win_id, + primary_border.win_id, + minor_border.win_id, + } + + clear_buf_on_leave(primary_bufnr) + + return { + bufnr = primary_bufnr, + win_id = primary_win_id, + + minor_bufnr = minor_bufnr, + minor_win_id = minor_win_id, + } +end + +--- Create window that takes up certain percentags of the current screen. +--- +--- Works regardless of current buffers, tabs, splits, etc. +--@param col_range number | Table: +-- If number, then center the window taking up this percentage of the screen. +-- If table, first index should be start, second_index should be end +--@param row_range number | Table: +-- If number, then center the window taking up this percentage of the screen. +-- If table, first index should be start, second_index should be end +--@param win_opts Table +--@param border_opts Table +function win_float.percentage_range_window(col_range, row_range, win_opts, border_opts) + win_opts = tbl.apply_defaults(win_opts, win_float.default_options) + + local default_win_opts = win_float.default_opts(win_opts) + default_win_opts.relative = "editor" + + local height_percentage, row_start_percentage + if type(row_range) == "number" then + assert(row_range <= 1) + assert(row_range > 0) + height_percentage = row_range + row_start_percentage = (1 - height_percentage) / 2 + elseif type(row_range) == "table" then + height_percentage = row_range[2] - row_range[1] + row_start_percentage = row_range[1] + else + error(string.format("Invalid type for 'row_range': %p", row_range)) + end + + default_win_opts.height = math.ceil(vim.o.lines * height_percentage) + default_win_opts.row = math.ceil(vim.o.lines * row_start_percentage) + + local width_percentage, col_start_percentage + if type(col_range) == "number" then + assert(col_range <= 1) + assert(col_range > 0) + width_percentage = col_range + col_start_percentage = (1 - width_percentage) / 2 + elseif type(col_range) == "table" then + width_percentage = col_range[2] - col_range[1] + col_start_percentage = col_range[1] + else + error(string.format("Invalid type for 'col_range': %p", col_range)) + end + + default_win_opts.col = math.floor(vim.o.columns * col_start_percentage) + default_win_opts.width = math.floor(vim.o.columns * width_percentage) + + local bufnr = win_opts.bufnr or vim.api.nvim_create_buf(false, true) + local win_id = vim.api.nvim_open_win(bufnr, true, default_win_opts) + vim.api.nvim_win_set_buf(win_id, bufnr) + + vim.cmd "setlocal nocursorcolumn" + vim.api.nvim_win_set_option(win_id, "winblend", win_opts.winblend) + + local border = Border:new(bufnr, win_id, default_win_opts, border_opts or {}) + + _AssociatedBufs[bufnr] = { win_id, border.win_id } + + clear_buf_on_leave(bufnr) + + return { + bufnr = bufnr, + win_id = win_id, + + border_bufnr = border.bufnr, + border_win_id = border.win_id, + } +end + +function win_float.clear(bufnr) + if _AssociatedBufs[bufnr] == nil then + return + end + + for _, win_id in ipairs(_AssociatedBufs[bufnr]) do + if vim.api.nvim_win_is_valid(win_id) then + vim.api.nvim_win_close(win_id, true) + end + end + + _AssociatedBufs[bufnr] = nil +end + +return win_float diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/window/init.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/window/init.lua new file mode 100644 index 0000000..e5b85e1 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/plenary/window/init.lua @@ -0,0 +1,16 @@ +local window = {} + +window.try_close = function(win_id, force) + if force == nil then + force = true + end + + pcall(vim.api.nvim_win_close, win_id, force) +end + +window.close_related_win = function(parent_win_id, child_win_id) + window.try_close(parent_win_id, true) + window.try_close(child_win_id, true) +end + +return window diff --git a/.config/nvim/pack/tree/start/plenary.nvim/lua/say.lua b/.config/nvim/pack/tree/start/plenary.nvim/lua/say.lua new file mode 100644 index 0000000..f1d7e54 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/lua/say.lua @@ -0,0 +1,61 @@ +local unpack = table.unpack or unpack + +local registry = { } +local current_namespace +local fallback_namespace + +local s = { + + _COPYRIGHT = "Copyright (c) 2012 Olivine Labs, LLC.", + _DESCRIPTION = "A simple string key/value store for i18n or any other case where you want namespaced strings.", + _VERSION = "Say 1.2", + + set_namespace = function(self, namespace) + current_namespace = namespace + if not registry[current_namespace] then + registry[current_namespace] = {} + end + end, + + set_fallback = function(self, namespace) + fallback_namespace = namespace + if not registry[fallback_namespace] then + registry[fallback_namespace] = {} + end + end, + + set = function(self, key, value) + registry[current_namespace][key] = value + end +} + +local __meta = { + __call = function(self, key, vars) + vars = vars or {} + + local str = registry[current_namespace][key] or registry[fallback_namespace][key] + + if str == nil then + return nil + end + str = tostring(str) + local strings = {} + + for i,v in ipairs(vars) do + table.insert(strings, tostring(v)) + end + + return #strings > 0 and str:format(unpack(strings)) or str + end, + + __index = function(self, key) + return registry[key] + end +} + +s:set_fallback('en') +s:set_namespace('en') + +s._registry = registry + +return setmetatable(s, __meta) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/plenary.nvim-scm-1.rockspec b/.config/nvim/pack/tree/start/plenary.nvim/plenary.nvim-scm-1.rockspec new file mode 100644 index 0000000..73a0f19 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/plenary.nvim-scm-1.rockspec @@ -0,0 +1,35 @@ +local _MODREV, _SPECREV = 'scm', '-1' +rockspec_format = "3.0" +package = 'plenary.nvim' +version = _MODREV .. _SPECREV + +description = { + summary = 'lua functions you don\'t want to write ', + labels = { "neovim" }, + detailed = [[ + plenary: full; complete; entire; absolute; unqualified. All the lua functions I don't want to write twice. + ]], + homepage = 'http://github.com/nvim-lua/plenary.nvim', + license = 'MIT/X11', +} + +dependencies = { + 'lua >= 5.1, < 5.4', + 'luassert' +} + +source = { + url = 'git://github.com/nvim-lua/plenary.nvim', +} + +build = { + type = 'builtin', + copy_directories = { + 'data', + 'plugin' + } +} +test = { + type = "command", + command = "make test" +} diff --git a/.config/nvim/pack/tree/start/plenary.nvim/plugin/plenary.vim b/.config/nvim/pack/tree/start/plenary.nvim/plugin/plenary.vim new file mode 100644 index 0000000..3fb7bc8 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/plugin/plenary.vim @@ -0,0 +1,9 @@ + +" Create command for running busted +command! -nargs=1 -complete=file PlenaryBustedFile + \ lua require('plenary.test_harness').test_file([[]]) + +command! -nargs=+ -complete=file PlenaryBustedDirectory + \ lua require('plenary.test_harness').test_directory_command([[]]) + +nnoremap PlenaryTestFile :lua require('plenary.test_harness').test_file(vim.fn.expand("%:p")) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/rockspec.template b/.config/nvim/pack/tree/start/plenary.nvim/rockspec.template new file mode 100644 index 0000000..c5070ef --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/rockspec.template @@ -0,0 +1,40 @@ +local git_tag = '$git_tag' +local modrev = '$modrev' +local specrev = '-1' + +local repo_url = '$repo_url' + +rockspec_format = '3.0' +package = '$package' +version = modrev .. specrev + +description = { + summary = '$summary', + detailed = $detailed_description, + labels = $labels, + homepage = '$homepage', + $license +} + +dependencies = { + 'lua >= 5.1, < 5.4', + 'luassert' +} + +source = { + url = repo_url .. '/archive/' .. git_tag .. '.zip', + dir = '$repo_name-' .. modrev, +} + +build = { + type = 'builtin', + copy_directories = { + 'data', + 'plugin' + } +} + +test = { + type = 'command', + command = 'make test' +} diff --git a/.config/nvim/pack/tree/start/plenary.nvim/scripts/generate_luassert_types.lua b/.config/nvim/pack/tree/start/plenary.nvim/scripts/generate_luassert_types.lua new file mode 100644 index 0000000..0785fe1 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/scripts/generate_luassert_types.lua @@ -0,0 +1,94 @@ +require "luassert" +local namespaces = require "luassert.namespaces" + +local reserved_words = { + "and", + "break", + "do", + "else", + "elseif", + "end", + "false", + "for", + "function", + "if", + "in", + "local", + "nil", + "not", + "or", + "repeat", + "return", + "then", + "true", + "until", + "while", +} + +local content = { + "---@meta", + "---This file is autogenerated, DO NOT EDIT", + 'error "Cannot require a meta file"', + "", + "---@generic T:any", + "---@alias LuassertFunction fun(value:T, message?:string):T", + "---@alias LuassertFunctionTwoArgs fun(expected:T, actual:T, message?:string):T", + "---@alias LuassertFunctionMultiArgs fun(...:T):T", + "", + "---@class Luassert", +} + +local args_count_per_matcher = { + matches = 2, + match = 2, + equal = 2, + equals = 2, + same = 2, + error_match = 2, + error_matches = 2, + matches_error = 2, + match_error = 2, + near = 3, +} + +local get_type_by_args = function(assertion) + local arg_count = args_count_per_matcher[assertion] or 1 + if arg_count == 1 then + return "LuassertFunction" + end + if arg_count == 2 then + return "LuassertFunctionTwoArgs" + end + return "LuassertFunctionMultiArgs" +end + +local fix_reserved_words = function(word) + if vim.tbl_contains(reserved_words, word) then + return table.concat { word:sub(1, 1):upper(), word:sub(2) } + end + return word +end + +local assertions = vim.tbl_keys(namespaces.assertion) +local modifiers = vim.tbl_keys(namespaces.modifier) + +table.sort(assertions) +table.sort(modifiers) + +for _, modifier in ipairs(modifiers) do + for _, assertion in ipairs(assertions) do + table.insert(content, ("---@field %s_%s %s"):format(modifier, assertion, get_type_by_args(assertion))) + end +end + +for _, assertion in ipairs(assertions) do + table.insert(content, ("---@field %s %s"):format(fix_reserved_words(assertion), get_type_by_args(assertion))) +end + +for _, modifier in ipairs(modifiers) do + table.insert(content, ("---@field %s Luassert"):format(fix_reserved_words(modifier))) +end + +local fd = assert(vim.loop.fs_open("./lua/plenary/_meta/_luassert.lua", "w", 438)) +assert(vim.loop.fs_write(fd, table.concat(content, "\n"), 0)) +assert(vim.loop.fs_close(fd)) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/scripts/minimal.vim b/.config/nvim/pack/tree/start/plenary.nvim/scripts/minimal.vim new file mode 100644 index 0000000..36acc45 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/scripts/minimal.vim @@ -0,0 +1,3 @@ + +set rtp+=. +runtime plugin/plenary.vim diff --git a/.config/nvim/pack/tree/start/plenary.nvim/scripts/update_filetypes_from_github.lua b/.config/nvim/pack/tree/start/plenary.nvim/scripts/update_filetypes_from_github.lua new file mode 100644 index 0000000..1a2225f --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/scripts/update_filetypes_from_github.lua @@ -0,0 +1,208 @@ +local FORCE_DOWNLOAD = false + +local log = require('plenary.log') + + +-- Defines all Languages known to GitHub. +-- +-- fs_name - Optional field. Only necessary as a replacement for the sample directory name if the +-- language name is not a valid filename under the Windows filesystem (e.g., if it +-- contains an asterisk). +-- type - Either data, programming, markup, prose, or nil +-- aliases - An Array of additional aliases (implicitly includes name.downcase) +-- ace_mode - A String name of the Ace Mode used for highlighting whenever +-- a file is edited. This must match one of the filenames in http://git.io/3XO_Cg. +-- Use "text" if a mode does not exist. +-- codemirror_mode - A String name of the CodeMirror Mode used for highlighting whenever a file is edited. +-- This must match a mode from https://git.io/vi9Fx +-- codemirror_mime_type - A String name of the file mime type used for highlighting whenever a file is edited. +-- This should match the `mime` associated with the mode from https://git.io/f4SoQ +-- wrap - Boolean wrap to enable line wrapping (default: false) +-- extensions - An Array of associated extensions (the first one is +-- considered the primary extension, the others should be +-- listed alphabetically) +-- filenames - An Array of filenames commonly associated with the language +-- interpreters - An Array of associated interpreters +-- searchable - Boolean flag to enable searching (defaults to true) +-- language_id - Integer used as a language-name-independent indexed field so that we can rename +-- languages in Linguist without reindexing all the code on GitHub. Must not be +-- changed for existing languages without the explicit permission of GitHub staff. +-- color - CSS hex color to represent the language. Only used if type is "programming" or "markup". +-- tm_scope - The TextMate scope that represents this programming +-- language. This should match one of the scopes listed in +-- the grammars.yml file. Use "none" if there is no grammar +-- for this language. +-- group - Name of the parent language. Languages in a group are counted +-- in the statistics as the parent language. + +local lyaml = require('lyaml') + +local Path = require('plenary.path') +local curl = require('plenary.curl') + +local write_file = function(path, string) + local fd = assert(vim.loop.fs_open(path, "w", 438)) + assert(vim.loop.fs_write(fd, string, 0)) + assert(vim.loop.fs_close(fd)) +end + +if FORCE_DOWNLOAD or not Path:new("build/languages.yml"):exists() then + local languages_yml = curl.get( + 'https://raw.githubusercontent.com/github/linguist/master/lib/linguist/languages.yml' + ).body + + write_file("build/languages.yml", languages_yml) +else + print("Using already downloaded file!") +end + +local prio = { + no_match = -1, + scope = 1, + alias = 2, + exact_match = 3, +} + +local find_filetype = function(name, linguist_info, filetype_set) + name = string.lower(name) + + local filetype, priority = nil, -1 + if filetype_set[name] then + filetype, priority = name, prio.exact_match + end + + if not filetype then + if linguist_info.aliases then + for _, ft in ipairs(linguist_info.aliases) do + ft = string.lower(ft) + if filetype_set[ft] then + filetype, priority = ft, prio.alias + + break + end + end + end + end + + if not filetype then + if linguist_info.tm_scope then + local tm_scope_split = vim.split(linguist_info.tm_scope, ".", true) + local tm_scope = tm_scope_split[#tm_scope_split] + + if filetype_set[tm_scope] then + filetype, priority = tm_scope, prio.scope + end + end + end + + return filetype, priority +end + +local overeager_filetypes = { + xml = true, +} + + +local parse_file = function() + local yml_string = Path:new("build/languages.yml"):read() + local yml_table = lyaml.load(yml_string) + + local output = { + extension = {}, + file_name = {} + } + local intervention = {} + + local filetype_completions = vim.fn.getcompletion('', 'filetype') + + local filetype_set = {} + for _, completed_ft in ipairs(filetype_completions) do + filetype_set[completed_ft] = true + end + + local add_extension = function(ext, filetype, priority) + -- If we have a better match, don't do it. + if output.extension[ext] then + if overeager_filetypes[output.extension[ext].filetype] then + log.debug("Overager:", output.extension[ext].filetype) + elseif output.extension[ext].priority > priority then + log.debug( + "Skipping:", ext, filetype, priority, + "due to existing:", output.extension[ext].priority, output.extension[ext].filetype + ) + + return + else + log.debug( + "Override:", ext, filetype, priority, + "due to existing:", output.extension[ext].priority, output.extension[ext].filetype + ) + end + end + + output.extension[ext] = { + filetype = filetype, + priority = priority, + } + end + + local add_filename = function(filename, filetype) + output.file_name[filename:lower()] = { + filetype = filetype:lower() + } + end + + for k, v in pairs(yml_table) do + local filetype, priority = find_filetype(k, v, filetype_set) + + if filetype then + if v.extensions then + for _, ext in ipairs(v.extensions) do + if ext:sub(1, 1) == '.' then + ext = ext:sub(2, #ext) + end + + add_extension(ext, filetype, priority) + end + end + + -- For stuff like 'Makefile' + -- This should go in a separate table + if v.filenames then + for _, fname in ipairs(v.filenames) do + add_filename(fname, filetype) + end + end + else + table.insert(intervention, v) + end + end + + -- P(intervention) + + local result = 'return {\n' + + result = result .. " extension = {\n" + for k, v in pairs(output.extension) do + result = result .. string.format(" ['%s'] = [[%s]],\n", k, v.filetype) + end + result = result .. ' },\n' + + result = result .. " file_name = {\n" + for k, v in pairs(output.file_name) do + result = result .. string.format(" ['%s'] = [[%s]],\n", k, v.filetype) + end + result = result .. ' },\n' + + result = result .. '}\n' + + return result +end + +print("Parsing File...") +local res = parse_file() +print("...Done") + +print("Writing File...") +write_file('./data/plenary/filetypes/base.lua', res) +print("...Done!") diff --git a/.config/nvim/pack/tree/start/plenary.nvim/scripts/update_vararg.py b/.config/nvim/pack/tree/start/plenary.nvim/scripts/update_vararg.py new file mode 100755 index 0000000..68f6733 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/scripts/update_vararg.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +import os +import subprocess + +def generate_file(name, outpath, **kwargs): + from jinja2 import Environment, FileSystemLoader + env = Environment(loader=FileSystemLoader('./vararg')) + template = env.get_template(name) + path = os.path.join(outpath, name) + with open(path, 'w') as fp: + fp.write(template.render(kwargs)) + subprocess.run(["lua-format", "-i", path]) + +if __name__ == '__main__': + generate_file('rotate.lua', '../lua/plenary/vararg', amount=16) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/scripts/vararg/rotate.lua b/.config/nvim/pack/tree/start/plenary.nvim/scripts/vararg/rotate.lua new file mode 100644 index 0000000..6790a9c --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/scripts/vararg/rotate.lua @@ -0,0 +1,30 @@ +---@brief [[ +---Do not edit this file, it was generated! +---Provides a function to rotate a lua vararg +---@brief ]] + +local tbl = require('plenary.tbl') + +local rotate_lookup = {} + +{% for n in range(1, amount) %} + rotate_lookup[{{n}}] = function ({% for n in range(n) %} A{{n}} {{ ", " if not loop.last else "" }} {% endfor %}) + return {% for n in range(1, n) %} A{{n}}, {% endfor %} A0 + end +{% endfor %} + +local function rotate_n(first, ...) + local n = select("#", ...) + 1 + local args = tbl.pack(...) + args[n] = first + return tbl.unpack(args, 1, n) +end + +local function rotate(nargs, ...) + if nargs == nil or nargs < 1 then + return + end + return (rotate_lookup[nargs] or rotate_n)(...) +end + +return rotate diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/manual/large_job_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/manual/large_job_spec.lua new file mode 100644 index 0000000..2ab72a2 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/manual/large_job_spec.lua @@ -0,0 +1,44 @@ +require("plenary.reload").reload_module "plenary" + +local Job = require "plenary.job" +local profiler = require "plenary.profile.lua_profiler" + +profiler.start() + +local start = vim.fn.reltime() +local finish = nil + +local results = {} + +local j = Job:new { + command = "fdfind", + + cwd = "~/plugins/", + + enable_handlers = false, + + on_stdout = function(_, data) + table.insert(results, data) + end, + + -- on_exit = vim.schedule_wrap(function() + -- finish = vim.fn.reltime(start) + -- end), +} + +pcall(function() + j:sync(2000, 5) +end) +finish = vim.fn.reltime(start) + +profiler.stop() +profiler.report "/home/tj/tmp/temp.txt" + +if finish == nil then + print "Did not finish :'(" +else + print("finished in:", vim.fn.reltimestr(finish)) +end + +collectgarbage() +print(collectgarbage "count") diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/minimal_init.vim b/.config/nvim/pack/tree/start/plenary.nvim/tests/minimal_init.vim new file mode 100644 index 0000000..f3e071b --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/minimal_init.vim @@ -0,0 +1,5 @@ + +set rtp+=. +runtime plugin/plenary.vim + +nnoremap ,,x :luafile % diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/async_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/async_spec.lua new file mode 100644 index 0000000..2fb2db3 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/async_spec.lua @@ -0,0 +1,53 @@ +require("plenary.async").tests.add_to_env() + +describe("async", function() + a.it("void functions can call wrapped functions", function() + local stat = 0 + local saved_arg + + local wrapped = a.wrap(function(inc, callback) + stat = stat + inc + callback() + end, 2) + + local voided = a.void(function(arg) + wrapped(1) + wrapped(2) + wrapped(3) + stat = stat + 1 + saved_arg = arg + end) + + voided "hello" + + assert(stat == 7) + assert(saved_arg == "hello") + end) + + a.it("void functions can call wrapped functions with ignored arguments", function() + local stat = 0 + local saved_arg + + local wrapped = a.wrap(function(inc, nil1, nil2, callback) + assert(type(inc) == "number") + assert(nil1 == nil) + assert(nil2 == nil) + assert(type(callback) == "function") + stat = stat + inc + callback() + end, 4) + + local voided = a.void(function(arg) + wrapped(1) + wrapped(2, nil) + wrapped(3, nil, nil) + stat = stat + 1 + saved_arg = arg + end) + + voided "hello" + + assert(stat == 7) + assert(saved_arg == "hello") + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/channel_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/channel_spec.lua new file mode 100644 index 0000000..df5f8ec --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/channel_spec.lua @@ -0,0 +1,219 @@ +require("plenary.async").tests.add_to_env() +local channel = a.control.channel +local eq = assert.are.same +local apcall = a.util.apcall + +describe("channel", function() + describe("oneshot", function() + a.it("should work when rx is used first", function() + local tx, rx = channel.oneshot() + + a.run(function() + local got = rx() + + eq("sent value", got) + end) + + tx "sent value" + end) + + a.it("should work when tx is used first", function() + local tx, rx = channel.oneshot() + + tx "sent value" + + local got = rx() + + eq("sent value", got) + end) + + a.it("should work with multiple returns", function() + local tx, rx = channel.oneshot() + + a.run(function() + local got, got2 = rx() + eq("sent value", got) + eq("another sent value", got2) + end) + + tx("sent value", "another sent value") + end) + + a.it("should work when sending a falsey value", function() + local tx, rx = channel.oneshot() + + tx(false) + + local res = rx() + eq(res, false) + + local stat, ret = apcall(rx) + eq(stat, false) + local stat, ret = apcall(rx) + eq(stat, false) + end) + + a.it("should work when sending a nil value", function() + local tx, rx = channel.oneshot() + + tx(nil) + + local res = rx() + eq(res, nil) + + local stat, ret = apcall(rx) + eq(stat, false) + local stat, ret = apcall(rx) + eq(stat, false) + end) + + a.it("should error when sending mulitple times", function() + local tx, rx = channel.oneshot() + + tx() + local stat = pcall(tx) + eq(stat, false) + end) + + a.it("should block receiving multiple times", function() + local tx, rx = channel.oneshot() + tx(true) + rx() + local stat = apcall(rx) + eq(stat, false) + end) + end) + + describe("mpsc", function() + a.it("should wait multiple recv before any send", function() + local sender, receiver = channel.mpsc() + + local expected_count = 10 + + a.run(function() + for i = 1, expected_count do + a.util.sleep(250) + sender.send(i) + end + end) + + local receive_count = 0 + while receive_count < expected_count do + receive_count = receive_count + 1 + local i = receiver.recv() + eq(receive_count, i) + end + end) + + a.it("should queues multiple sends before any read", function() + local sender, receiver = channel.mpsc() + + local counter = 0 + + a.run(function() + counter = counter + 1 + sender.send(10) + + counter = counter + 1 + sender.send(20) + end) + + a.util.sleep(1000) + + eq(10, receiver.recv()) + eq(20, receiver.recv()) + eq(2, counter) + end) + + a.it("should queues multiple sends from multiple producers before any read", function() + local sender, receiver = channel.mpsc() + + local counter = 0 + + a.run(function() + counter = counter + 1 + sender.send(10) + + counter = counter + 1 + sender.send(20) + end) + + a.run(function() + counter = counter + 1 + sender.send(30) + + counter = counter + 1 + sender.send(40) + end) + + a.util.sleep(1000) + + local read_counter = 0 + a.util.block_on(function() + for _ = 1, 4 do + receiver.recv() + read_counter = read_counter + 1 + end + end, 1000) + eq(4, counter) + eq(4, read_counter) + end) + + a.it("should read only the last value", function() + local sender, receiver = channel.mpsc() + + local counter = 0 + + a.run(function() + counter = counter + 1 + sender.send(10) + + counter = counter + 1 + sender.send(20) + end) + + a.util.sleep(1000) + + eq(20, receiver.last()) + eq(2, counter) + end) + end) + + describe("counter", function() + a.it("should work", function() + local tx, rx = channel.counter() + + tx.send() + tx.send() + tx.send() + + local counter = 0 + + a.run(function() + for i = 1, 3 do + rx.recv() + counter = counter + 1 + end + end) + + eq(counter, 3) + end) + + a.it("should work when getting last", function() + local tx, rx = channel.counter() + + tx.send() + tx.send() + tx.send() + + local counter = 0 + + a.run(function() + rx.last() + counter = counter + 1 + end) + + eq(counter, 1) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/condvar_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/condvar_spec.lua new file mode 100644 index 0000000..b677762 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/condvar_spec.lua @@ -0,0 +1,158 @@ +require("plenary.async").tests.add_to_env() +local Condvar = a.control.Condvar +local eq = assert.are.same +local join, run_all = a.util.join, a.util.run_all + +describe("condvar", function() + a.it("should allow blocking", function() + local var = false + + local condvar = Condvar.new() + + a.run(function() + condvar:wait() + var = true + end) + + eq(var, false) + + condvar:notify_one() + + eq(var, true) + end) + + a.it("should be able to notify one when running", function() + local counter = 0 + + local condvar = Condvar.new() + + local first = function() + condvar:wait() + counter = counter + 1 + end + + local second = function() + condvar:wait() + counter = counter + 1 + end + + local third = function() + condvar:wait() + counter = counter + 1 + end + + a.run(function() + join { first, second, third } + end) + + eq(0, counter) + + condvar:notify_one() + + eq(1, counter) + + condvar:notify_one() + + eq(counter, 2) + + condvar:notify_one() + + eq(counter, 3) + end) + + a.it("should allow notify_one to work when using await_all", function() + local counter = 0 + + local condvar = Condvar.new() + + local first = function() + condvar:wait() + counter = counter + 1 + end + + local second = function() + condvar:wait() + counter = counter + 1 + end + + local third = function() + condvar:wait() + counter = counter + 1 + end + + run_all { first, second, third } + + eq(0, counter) + + condvar:notify_one() + + eq(1, counter) + + condvar:notify_one() + + eq(counter, 2) + + condvar:notify_one() + + eq(counter, 3) + end) + + a.it("should notify_all", function() + local counter = 0 + + local condvar = Condvar.new() + + local first = function() + condvar:wait() + counter = counter + 1 + end + + local second = function() + condvar:wait() + counter = counter + 1 + end + + local third = function() + condvar:wait() + counter = counter + 1 + end + + run_all { first, second, third } + + eq(0, counter) + + condvar:notify_all() + + eq(3, counter) + end) + + a.it("notify all works multiple times", function() + local condvar = Condvar.new() + local counter = 0 + + a.run(function() + condvar:wait() + counter = counter + 1 + end) + + a.run(function() + condvar:wait() + counter = counter + 1 + end) + + eq(0, counter) + + condvar:notify_all() + + eq(2, counter) + + a.run(function() + condvar:wait() + counter = 0 + end) + + condvar:notify_all() + + eq(0, counter) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/deque_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/deque_spec.lua new file mode 100644 index 0000000..3cf729b --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/deque_spec.lua @@ -0,0 +1,91 @@ +local Deque = require("plenary.async.structs").Deque +local eq = assert.are.same + +-- just a helper to create the test deque +local function new_deque() + local deque = Deque.new() + eq(deque:len(), 0) + + deque:pushleft(1) + eq(deque:len(), 1) + + deque:pushleft(2) + eq(deque:len(), 2) + + deque:pushright(3) + eq(deque:len(), 3) + + deque:pushright(4) + eq(deque:len(), 4) + + deque:pushright(5) + eq(deque:len(), 5) + + return deque +end + +describe("deque", function() + it("should allow pushing and popping and finding len", function() + new_deque() + end) + + it("should be able to iterate from left", function() + local deque = new_deque() + + local iter = deque:ipairs_left() + + local i, v = iter() + eq(i, -2) + eq(v, 2) + + i, v = iter() + eq(i, -1) + eq(v, 1) + + i, v = iter() + eq(i, 0) + eq(v, 3) + + i, v = iter() + eq(i, 1) + eq(v, 4) + + i, v = iter() + eq(i, 2) + eq(v, 5) + end) + + it("should be able to iterate from right", function() + local deque = new_deque() + + local iter = deque:ipairs_right() + + local i, v = iter() + eq(i, 2) + eq(v, 5) + + i, v = iter() + eq(i, 1) + eq(v, 4) + + i, v = iter() + eq(i, 0) + eq(v, 3) + + i, v = iter() + eq(i, -1) + eq(v, 1) + + i, v = iter() + eq(i, -2) + eq(v, 2) + end) + + it("should allow clearing", function() + local deque = new_deque() + + deque:clear() + + assert(deque:is_empty()) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/semaphore_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/semaphore_spec.lua new file mode 100644 index 0000000..1342350 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/semaphore_spec.lua @@ -0,0 +1,58 @@ +require("plenary.async").tests.add_to_env() +local Semaphore = a.control.Semaphore + +local eq = assert.are.same + +describe("semaphore", function() + a.it("should validate arguments", function() + local status = pcall(Semaphore.new, -1) + eq(status, false) + + local status = pcall(Semaphore.new) + eq(status, false) + end) + + a.it("should acquire a permit if available", function() + local sem = Semaphore.new(1) + local permit = sem:acquire() + assert(permit ~= nil) + end) + + a.it("should block if no permit is available", function() + local sem = Semaphore.new(1) + sem:acquire() + + local completed = false + local blocking = function() + sem:acquire() + completed = true + end + a.run(blocking) + + eq(completed, false) + end) + + a.it("should give another permit when an acquired permit is released", function() + local sem = Semaphore.new(1) + local permit = sem:acquire() + permit:forget() + local next_permit = sem:acquire() + assert(next_permit ~= nil) + end) + + a.it("should permit the next waiting client when a permit is released", function() + local sem = Semaphore.new(1) + local permit = sem:acquire() + + local completed = false + local blocking = function() + sem:acquire() + completed = true + end + + a.run(blocking) + permit:forget() + + eq(completed, true) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/test_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/test_spec.lua new file mode 100644 index 0000000..334910e --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/test_spec.lua @@ -0,0 +1,58 @@ +require("plenary.async").tests.add_to_env() + +a.describe("a.before_each", function() + local counter = 0 + + local set_counter_to_one = a.wrap(function(callback) + a.util.sleep(5) + counter = 1 + end, 1) + + a.before_each(a.void(function() + set_counter_to_one() + end)) + + a.it("should run in async context", function() + counter = counter + 1 + assert.are.same(counter, 2) + end) + + a.it("should run for all tests", function() + counter = counter + 2 + assert.are.same(counter, 3) + end) +end) + +a.describe("a.after_each", function() + local counter = 0 + + local set_counter_to_one = a.wrap(function(callback) + a.util.sleep(5) + counter = 1 + end, 1) + + a.after_each(a.void(function() + set_counter_to_one() + end)) + + a.it("should not run before first test", function() + counter = counter + 1 + assert.are.same(counter, 1) + end) + + a.it("should run before the second test", function() + counter = counter + 2 + assert.are.same(counter, 3) + end) + + a.it("should run before the third test", function() + counter = counter + 3 + assert.are.same(counter, 4) + end) +end) + +a.describe("a.pending", function() + a.pending("This test is disabled", function() + assert(false, "Should not run") + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/util_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/util_spec.lua new file mode 100644 index 0000000..94ae324 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async/util_spec.lua @@ -0,0 +1,77 @@ +require("plenary.async").tests.add_to_env() +local block_on = a.util.block_on +local eq = assert.are.same +local id = a.util.id + +describe("async await util", function() + describe("block_on", function() + a.it("should block_on", function() + local fn = function() + a.util.sleep(100) + return "hello" + end + + local res = fn() + eq(res, "hello") + end) + + a.it("should work even when failing", function() + local nonleaf = function() + eq(true, false) + end + + local stat = pcall(block_on, nonleaf) + eq(stat, false) + end) + end) + + describe("protect", function() + a.it("should be able to protect a non-leaf future", function() + local nonleaf = function() + error "This should error" + return "return" + end + + local stat, ret = pcall(nonleaf) + eq(false, stat) + assert(ret:match "This should error") + end) + + a.it("should be able to protect a non-leaf future that doesnt fail", function() + local nonleaf = function() + return "didnt fail" + end + + local stat, ret = pcall(nonleaf) + eq(stat, true) + eq(ret, "didnt fail") + end) + end) + + local function sleep(msec) + return function() + a.util.sleep(msec) + return msec + end + end + + describe("race", function() + a.it("should return the first result", function() + local funcs = vim.tbl_map(sleep, { 300, 400, 100, 200 }) + local result = a.util.race(funcs) + eq(result, 100) + end) + end) + + describe("run_first", function() + a.it("should return the first result", function() + local async_functions = vim.tbl_map(function(num) + return function(callback) + return a.run(sleep(num), callback) + end + end, { 300, 400, 100, 200 }) + local result = a.util.run_first(async_functions) + eq(result, 100) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async_testing_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async_testing_spec.lua new file mode 100644 index 0000000..c1c991b --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/async_testing_spec.lua @@ -0,0 +1,75 @@ +local Job = require "plenary.job" + +local Timing = {} + +function Timing:log(name) + self[name] = vim.loop.uptime() +end + +function Timing:check(from, to, min_elapsed) + assert(self[from], "did not log " .. from) + assert(self[to], "did not log " .. to) + local elapsed = self[to] - self[from] + assert( + min_elapsed <= elapsed, + string.format("only took %s to get from %s to %s - expected at least %s", elapsed, from, to, min_elapsed) + ) +end + +describe("Async test", function() + it("can resume testing with vim.defer_fn", function() + local co = coroutine.running() + assert(co, "not running inside a coroutine") + + local timing = setmetatable({}, { __index = Timing }) + + vim.defer_fn(function() + coroutine.resume(co) + end, 200) + timing:log "before" + coroutine.yield() + timing:log "after" + timing:check("before", "after", 0.1) + end) + + it("can resume testing from job callback", function() + local co = coroutine.running() + assert(co, "not running inside a coroutine") + + local timing = setmetatable({}, { __index = Timing }) + + Job:new({ + command = "bash", + args = { + "-ce", + [[ + sleep 0.2 + echo hello + sleep 0.2 + echo world + sleep 0.2 + exit 42 + ]], + }, + on_stdout = function(_, data) + timing:log(data) + end, + on_exit = function(_, exit_status) + timing:log "exit" + --This is required so that the rest of the test will run in a proper context + vim.schedule(function() + coroutine.resume(co, exit_status) + end) + end, + }):start() + timing:log "job started" + local exit_status = coroutine.yield() + timing:log "job finished" + assert.are.equal(exit_status, 42) + + timing:check("job started", "job finished", 0.3) + timing:check("job started", "hello", 0.1) + timing:check("hello", "world", 0.1) + timing:check("world", "job finished", 0.1) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/context_manager_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/context_manager_spec.lua new file mode 100644 index 0000000..54de82b --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/context_manager_spec.lua @@ -0,0 +1,203 @@ +local context_manager = require "plenary.context_manager" +local debug_utils = require "plenary.debug_utils" +local Path = require "plenary.path" + +local with = context_manager.with +local open = context_manager.open + +local README_STR_PATH = vim.fn.fnamemodify(debug_utils.sourced_filepath(), ":h:h:h") .. "/README.md" +local README_FIRST_LINE = "# plenary.nvim" + +describe("context_manager", function() + it("works with objects", function() + local obj_manager = { + enter = function(self) + self.result = 10 + return self.result + end, + + exit = function() end, + } + + local result = with(obj_manager, function(obj) + return obj + end) + + assert.are.same(10, result) + assert.are.same(obj_manager.result, result) + end) + + it("works with coroutine", function() + local co = function() + coroutine.yield(10) + end + + local result = with(co, function(obj) + return obj + end) + + assert.are.same(10, result) + end) + + it("does not work with coroutine with extra yields", function() + local co = function() + coroutine.yield(10) + + -- Can't yield twice. That'd be bad and wouldn't make any sense. + coroutine.yield(10) + end + + assert.has.error_match(function() + with(co, function(obj) + return obj + end) + end, "Should not yield anymore, otherwise that would make things complicated") + end) + + it("reads from files with open", function() + local result = with(open(README_STR_PATH), function(reader) + return reader:read() + end) + + assert.are.same(result, README_FIRST_LINE) + end) + + it("reads from Paths with open", function() + local p = Path:new(README_STR_PATH) + + local result = with(open(p), function(reader) + return reader:read() + end) + + assert.are.same(result, README_FIRST_LINE) + end) + + it("calls exit on error with objects", function() + local entered = false + local exited = false + local obj_manager = { + enter = function(self) + entered = true + end, + + exit = function(self) + exited = true + end, + } + + assert.has.error_match(function() + with(obj_manager, function(obj) + assert(false, "failed in callback") + end) + end, "failed in callback") + + assert.is["true"](entered) + assert.is["true"](exited) + end) + + it("calls exit on error with coroutines", function() + local entered = false + local exited = false + local co = function() + entered = true + coroutine.yield(nil) + + exited = true + end + + assert.has.error_match(function() + with(co, function(obj) + assert(false, "failed in callback") + end) + end, "failed in callback") + + assert.is["true"](entered) + assert.is["true"](exited) + end) + + it("fails from enter error with objects", function() + local exited = false + local obj_manager = { + enter = function(self) + assert(false, "failed in enter") + end, + + exit = function(self) + exited = true + end, + } + + local ran_callback = false + assert.has.error_match(function() + with(obj_manager, function(obj) + ran_callback = true + end) + end, "failed in enter") + + assert.is["false"](ran_callback) + assert.is["false"](exited) + end) + + it("fails from enter error with coroutines", function() + local exited = false + local co = function() + assert(false, "failed in enter") + coroutine.yield(nil) + + exited = true + end + + local ran_callback = false + assert.has.error_match(function() + with(co, function(obj) + ran_callback = true + end) + end, "Should have yielded in coroutine.") + + assert.is["false"](ran_callback) + assert.is["false"](exited) + end) + + it("fails from exit error with objects", function() + local entered = false + local obj_manager = { + enter = function(self) + entered = true + end, + + exit = function(self) + assert(false, "failed in exit") + end, + } + + local ran_callback = false + assert.has.error_match(function() + with(obj_manager, function(obj) + ran_callback = true + end) + end, "failed in exit") + + assert.is["true"](entered) + assert.is["true"](ran_callback) + end) + + it("fails from exit error with coroutines", function() + local entered = false + local co = function() + entered = true + coroutine.yield(nil) + + assert(false, "failed in exit") + end + + local ran_callback = false + assert.has.error_match(function() + with(co, function(obj) + ran_callback = true + end) + end, "Should be done") + + assert.is["true"](entered) + assert.is["true"](ran_callback) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/curl_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/curl_spec.lua new file mode 100644 index 0000000..c4ba89a --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/curl_spec.lua @@ -0,0 +1,221 @@ +local curl = require "plenary.curl" +local eq = assert.are.same +local incl = function(p, s) + return (nil ~= string.find(s, p)) +end + +describe("CURL Wrapper:", function() + describe("request", function() ----------------------------------------------- + it("sends and returns table.", function() + eq( + "table", + type(curl.request { + url = "https://postman-echo.com/get", + method = "get", + accept = "application/json", + }) + ) + end) + + it("should accept the url as first argument.", function() + local res = curl.get("https://postman-echo.com/get", { + accept = "application/json", + }) + eq(200, res.status) + end) + end) + + describe("GET", function() -------------------------------------------------- + it("sends and returns table.", function() + eq( + "table", + type(curl.get { + url = "https://postman-echo.com/get", + accept = "application/json", + }) + ) + end) + + it("should accept the url as first argument.", function() + local res = curl.get("https://postman-echo.com/get", { + accept = "application/json", + }) + eq(200, res.status) -- table has response status + end) + + it("sends encoded URL query params.", function() + local query = { name = "john Doe", key = "123456" } + local response = curl.get("https://postman-echo.com/get", { + query = query, + }) + + eq(200, response.status) + eq(query, vim.fn.json_decode(response.body).args) + end) + + it("downloads files to opts.output synchronously", function() + local file = "https://media2.giphy.com/media/bEMcuOG3hXVnihvB7x/giphy.gif" + local loc = "/tmp/giphy2.gif" + local res = curl.get(file, { output = loc }) + + eq(1, vim.fn.filereadable(loc), "should exists") + eq(200, res.status, "should return 200") + eq(0, res.exit, "should have exit code of 0") + vim.fn.delete(loc) + end) + + it("downloads files to to opts.output asynchronous", function() + local res = nil + local succ = nil + local done = false + local file = "https://media2.giphy.com/media/notvalid.gif" + local loc = "/tmp/notvalid.gif" + + curl.get(file, { + output = loc, + callback = function(out) + done = true + succ = out.status == 200 + res = out + end, + }) + + vim.wait(60000, function() + return done + end) + + eq(403, res.status, "It should return 403") + assert(not succ, "It should fail") + + vim.fn.delete(loc) + end) + + it("sends with basic-auth as string", function() + local url = "https://postman-echo.com/basic-auth" + local auth, res + + auth = "postman:password" + res = curl.get(url, { auth = auth }) + assert(incl("authenticated.*true", res.body)) + eq(200, res.status) + + auth = "tami5:123456" + res = curl.get(url, { auth = auth }) + assert(not incl("authenticated.*true", res.body), "it should fail") + eq(401, res.status) + end) + + it("sends with basic-auth as table", function() + local url = "https://postman-echo.com/basic-auth" + local res = curl.get(url, { auth = { postman = "password" } }) + assert(incl("authenticated.*true", res.body)) + eq(200, res.status) + end) + end) + + describe("POST", function() -------------------------------------------------- + it("sends raw string", function() + local res = curl.post("https://postman-echo.com/post", { + body = "John Doe", + }) + assert(incl("John", res.body)) + eq(200, res.status) + end) + + it("sends lua table", function() + local res = curl.post("https://jsonplaceholder.typicode.com/posts", { + body = { + title = "Hello World", + body = "...", + }, + }) + eq(201, res.status) + end) + + it("sends file", function() + local res = curl.post("https://postman-echo.com/post", { + body = "./README.md", + }).body + + assert(incl("plenary.test_harness", res)) + end) + + it("sends and recives json body.", function() + local json = { title = "New", name = "YORK" } + local res = curl.post("https://postman-echo.com/post", { + body = vim.fn.json_encode(json), + headers = { + content_type = "application/json", + }, + }).body + eq(json, vim.fn.json_decode(res).json) + end) + + it("should not include the body twice", function() + local json = { title = "New", name = "YORK" } + local body = vim.fn.json_encode(json) + local res = curl.post("https://postman-echo.com/post", { + body = body, + headers = { + content_type = "application/json", + }, + dry_run = true, + }) + local joined_response = table.concat(res, " ") + local first_index = joined_response:find(body) + + eq(nil, joined_response:find(body, first_index + 1)) + end) + end) + describe("PUT", function() -------------------------------------------------- + it("sends changes and get be back the new version.", function() + local cha = { title = "New Title" } + local res = curl.put("https://jsonplaceholder.typicode.com/posts/8", { + body = cha, + }) + eq(cha.title, vim.fn.json_decode(res.body).title) + eq(200, res.status) + end) + end) + + describe("PATCH", function() ------------------------------------------------ + it("sends changes and get be back the new version.", function() + local cha = { title = "New Title" } + local res = curl.patch("https://jsonplaceholder.typicode.com/posts/8", { + body = cha, + }) + eq(cha.title, vim.fn.json_decode(res.body).title) + eq(200, res.status) + end) + end) + + describe("DELETE", function() ------------------------------------------------ + it("sends delete request", function() + local res = curl.delete "https://jsonplaceholder.typicode.com/posts/8" + eq(200, res.status) + end) + end) + + describe("DEBUG", function() -------------------------------------------------- + it("dry_run return the curl command to be ran.", function() + local res = curl.delete("https://jsonplaceholder.typicode.com/posts/8", { dry_run = true }) + assert(type(res) == "table") + end) + end) + + describe("Issue #601", function() -------------------------------------------- + it("should not use URL from previous call", function() + local url = "https://example.com" + local opts = { dry_run = true, dump = "" } -- dump would be random each time + local first = curl.get(url, opts) + eq(table.remove(first, #first), url, "expected url last") + + local success, second = pcall(curl.get, opts) + if success then + eq(first, second, "should be same, but without url") + else + -- Failure is also acceptable + end + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/enum_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/enum_spec.lua new file mode 100644 index 0000000..d4dcf93 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/enum_spec.lua @@ -0,0 +1,93 @@ +local Enum = require "plenary.enum" + +local function should_fail(fun) + local stat = pcall(fun) + assert(not stat, "Function should fail") +end + +describe("Enum", function() + it("should be able to define specific values for members", function() + local E = Enum { + { "Foo", 2 }, + { "Bar", 4 }, + "Qux", + "Baz", + { "Another", 11 }, + } + + assert(E.Foo.value == 2) + assert(E.Bar.value == 4) + assert(E.Qux.value == 5) + assert(E.Baz.value == 6) + assert(E.Another.value == 11) + + assert(E[2] == "Foo") + assert(E[4] == "Bar") + assert(E[5] == "Qux") + assert(E[6] == "Baz") + assert(E[11] == "Another") + end) + it("should compare with itself", function() + local E1 = Enum { + "Foo", + { "Qux", 11 }, + "Bar", + "Baz", + } + + local E2 = Enum { + "Foo", + "Bar", + "Baz", + } + + assert(E1.Foo < E1.Qux) + assert(E1.Baz > E1.Bar) + + assert(not (E1.Foo == E2.Foo)) + + should_fail(function() + return E1.Foo > E2.Foo + end) + + should_fail(function() + return E2.Bar >= E1.Foo + end) + end) + it("should error when accessing invalid field", function() + local E = Enum { + "Foo", + "Bar", + "Baz", + } + + should_fail(function() + return E.foo + end) + + should_fail(function() + return E.bar + end) + end) + it("should fail if there is name or index clashing", function() + should_fail(function() + return Enum { + "Foo", + "Foo", + } + end) + should_fail(function() + return Enum { + "Foo", + { "Bar", 1 }, + } + end) + end) + it("should fail if there is a key that starts with lowercase", function() + should_fail(function() + return Enum { + "foo", + } + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/filetype_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/filetype_spec.lua new file mode 100644 index 0000000..af85bd5 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/filetype_spec.lua @@ -0,0 +1,139 @@ +local filetype = require "plenary.filetype" + +describe("filetype", function() + describe("_get_extension_parts", function() + it("should find stuff with underscores", function() + assert.are.same({ "py" }, filetype._get_extension_parts "__init__.py") + end) + + it("should find all possibilities", function() + assert.are.same({ "rst.txt", "txt" }, filetype._get_extension_parts "example.rst.txt") + assert.are.same({ "emacs.desktop", "desktop" }, filetype._get_extension_parts "example.emacs.desktop") + end) + end) + + describe("detect_from_extension", function() + it("should work for md", function() + assert.are.same("markdown", filetype.detect_from_extension "Readme.md") + end) + + it("should work for CMakeList.txt", function() + assert.are.same("text", filetype.detect_from_extension "CMakeLists.txt") + end) + + it("should work with extensions with dot", function() + assert.are.same("rst", filetype.detect_from_extension "example.rst.txt") + assert.are.same("rst", filetype.detect_from_extension "example.rest.txt") + assert.are.same("yaml", filetype.detect_from_extension "example.yaml.sed") + assert.are.same("yaml", filetype.detect_from_extension "example.yml.mysql") + assert.are.same("erlang", filetype.detect_from_extension "asdf/example.app.src") + assert.are.same("cmake", filetype.detect_from_extension "/asdf/example.cmake.in") + assert.are.same("desktop", filetype.detect_from_extension "/asdf/asdf.desktop.in") + assert.are.same("xml", filetype.detect_from_extension "example.dll.config") + assert.are.same("haml", filetype.detect_from_extension "example.haml.deface") + assert.are.same("html", filetype.detect_from_extension "example.html.hl") + assert.are.same("yaml", filetype.detect_from_extension "example.model.lkml") + assert.are.same("rust", filetype.detect_from_extension "example.rs.in") + assert.are.same("sh", filetype.detect_from_extension "example.sh.in") + assert.are.same("json", filetype.detect_from_extension "example.tfstate.backup") + assert.are.same("yaml", filetype.detect_from_extension "example.view.lkml") + assert.are.same("xml", filetype.detect_from_extension "example.xml.dist") + assert.are.same("xml", filetype.detect_from_extension "example.xsp.metadata") + end) + + it("should work for ext==ft even without a table value", function() + assert.are.same("bib", filetype.detect_from_extension "file.bib") + assert.are.same("bst", filetype.detect_from_extension "file.bst") + end) + end) + + describe("detect_from_name", function() + it("should work for common filenames, like makefile", function() + assert.are.same("make", filetype.detect_from_name "Makefile") + assert.are.same("make", filetype.detect_from_name "makefile") + end) + + it("should work for CMakeList.txt", function() + assert.are.same("cmake", filetype.detect_from_name "CMakeLists.txt") + end) + end) + + describe("detect_from_modeline", function() + it("should work for modeline 2", function() + assert.are.same("help", filetype._parse_modeline " vim:tw=78:ts=8:noet:ft=help:norl:") + end) + + it("should return nothing if ft not found in modeline", function() + assert.are.same("", filetype._parse_modeline "/* vim: set ts=8 sw=4 tw=0 noet : */") + end) + + it("should return nothing for random line", function() + assert.are.same("", filetype._parse_modeline "return filetype") + end) + end) + + describe("detect_from_shebang", function() + it("should work for shell", function() + assert.are.same("sh", filetype._parse_shebang "#!/bin/sh") + end) + + it("should work for bash", function() + assert.are.same("sh", filetype._parse_shebang "#!/bin/bash") + end) + + it("should work for usr/bin/env shell", function() + assert.are.same("sh", filetype._parse_shebang "#!/usr/bin/env sh") + end) + + it("should work for env shell", function() + assert.are.same("sh", filetype._parse_shebang "#!/bin/env sh") + end) + + it("should work for python", function() + assert.are.same("python", filetype._parse_shebang "#!/bin/python") + end) + + it("should work for /usr/bin/python3", function() + assert.are.same("python", filetype._parse_shebang "#!/usr/bin/python3") + end) + + it("should work for python3", function() + assert.are.same("python", filetype._parse_shebang "#!/bin/python3") + end) + + it("should work for env python", function() + assert.are.same("python", filetype._parse_shebang "#!/bin/env python") + end) + + it("should not work for random line", function() + assert.are.same("", filetype._parse_shebang 'local path = require"plenary.path"') + end) + end) + + describe("detect", function() + it("should work for common filetypes, like python", function() + assert.are.same("python", filetype.detect "__init__.py") + end) + + it("should work for common filenames, like makefile", function() + assert.are.same("make", filetype.detect "Makefile") + assert.are.same("make", filetype.detect "makefile") + end) + + it("should work for CMakeList.txt", function() + assert.are.same("cmake", filetype.detect "CMakeLists.txt") + end) + + it("should work for common files, even with .s, like .bashrc", function() + assert.are.same("sh", filetype.detect ".bashrc") + end) + + it("should work fo custom filetypes, like fennel", function() + assert.are.same("fennel", filetype.detect "init.fnl") + end) + + it("should work for custom filenames, like Cakefile", function() + assert.are.same("coffee", filetype.detect "Cakefile") + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/functional_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/functional_spec.lua new file mode 100644 index 0000000..9a34b22 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/functional_spec.lua @@ -0,0 +1,18 @@ +local f = require "plenary.functional" + +describe("functional", function() + describe("partial", function() + local function args(...) + assert.is.equal(4, select("#", ...)) + return table.concat({ ... }, ",") + end + it("should bind correct parameters", function() + local expected = args(1, 2, 3, 4) + assert.is.equal(expected, f.partial(args)(1, 2, 3, 4)) + assert.is.equal(expected, f.partial(args, 1)(2, 3, 4)) + assert.is.equal(expected, f.partial(args, 1, 2)(3, 4)) + assert.is.equal(expected, f.partial(args, 1, 2, 3)(4)) + assert.is.equal(expected, f.partial(args, 1, 2, 3, 4)()) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/iterators_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/iterators_spec.lua new file mode 100644 index 0000000..8d553bf --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/iterators_spec.lua @@ -0,0 +1,149 @@ +local i = require "plenary.iterators" +local f = require "plenary.functional" +local eq = assert.are.same + +local function check_keys(tbl, keys) + for _, key in ipairs(keys) do + if not tbl[key] then + error("Key " .. key .. " was not found") + end + end +end + +describe("iterators", function() + it("should be able to create iterator from table", function() + local tbl = { first = 1, second = 2, third = 3, fourth = 4 } + local results = i.iter(tbl):tolist() + eq(#results, 4) + check_keys(tbl, { "first", "second", "third", "fourth" }) + results = { + { "first", 1 }, + { "second", 2 }, + { "third", 3 }, + { "fourth", 4 }, + } + end) + + it("should be able to create iterator from array", function() + local tbl = { 1, 2, 3, 4 } + local results = i.iter(tbl):tolist() + eq(#results, 4) + check_keys(tbl, { 1, 2, 3, 4 }) + end) + + it("should be able to fold", function() + local numbers = { 1, 2, 3, 4 } + local result = i.iter(numbers):fold(0, function(a, v) + return a + v + end) + eq(result, 10) + + local strings = { "hello", "world", "this", "is", "a", "test" } + result = i.iter(strings):fold("", function(a, v) + return a .. v + end) + eq(result, "helloworldthisisatest") + end) + + it("should be able to enumerate", function() + local tbl = { 1, 2, 3, 4 } + local results = i.iter(tbl) + :enumerate() + :map(function(idx, v) + return { idx, v } + end) + :tolist() + eq(#results, 4) + eq(results[1], { 1, 1 }) + eq(results[2], { 2, 2 }) + eq(results[3], { 3, 3 }) + eq(results[4], { 4, 4 }) + end) + + it("should be able to find", function() + local tbl = { 1, 2, 3, 4 } + local tbl_iter = i.iter(tbl) + local res = tbl_iter:find(2) + eq(res, 2) + res = tbl_iter:find "will not find this" + assert(not res) + + tbl = { 1, 2, 3, 4, "some random string", 6 } + eq( + i.iter(tbl):find(function(x) + return type(x) == "string" + end), + "some random string" + ) + end) + + it("should be table to chain", function() + local first = i.iter { 1, 2, 3 } + local second = i.iter { 4, 5, 6, 7 } + local third = i.iter { 8, 9, 10 } + local res = (first .. second .. third):tolist() + eq(res, i.range(10):tolist()) + end) + + it("should make a range", function() + eq({ 1, 2, 3, 4, 5 }, i.range(5):tolist()) + end) + + it("should be able to make a stateful", function() + local iter = i.range(3):stateful() + eq(iter(), 1) + eq(iter(), 2) + eq(iter(), 3) + assert(not iter()) + assert(not iter()) + assert(not iter()) + + local iter = i.range(3):stateful() + -- dump(iter:tolist()) + eq(iter:tolist(), { 1, 2, 3 }) + end) + + it("should be able to flatten", function() + local iter = i.range(3) + :map(function(_) + return i.iter { 5, 7, 9 } + end) + :flatten() + :stateful() + eq(iter(), 5) + eq(iter(), 7) + eq(iter(), 9) + eq(iter(), 5) + eq(iter(), 7) + eq(iter(), 9) + eq(iter(), 5) + eq(iter(), 7) + eq(iter(), 9) + local iter = i.range(3) + :map(function(_) + return i.iter { 5, 7, 9 } + end) + :flatten() + eq(iter:tolist(), { 5, 7, 9, 5, 7, 9, 5, 7, 9 }) + end) + + it("should be able to flatten very nested stuff", function() + local iter = i.iter({ 5, 6, i.iter { i.iter { 5, 5 }, 7, 8 }, i.iter { 9, 10, i.iter { 1, 2 } } }):flatten() + eq(iter:tolist(), { 5, 6, 5, 5, 7, 8, 9, 10, 1, 2 }) + end) + + it("chaining nil should work", function() + local iter = i.iter(""):chain(i.iter { 5, 7, 9 }) + eq(#iter:tolist(), 3) + end) + + describe("generators", function() + it("should be able to split", function() + local iter = i.split(" hello person world ", " ") + eq(iter:tolist(), { "", "hello", "person", "world", "" }) + + iter = i.split("\n\n\nfirst\nsecond\nthird\n\n", "\n") + eq(iter:tolist(), { "", "", "", "first", "second", "third", "", "" }) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/job/validation_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/job/validation_spec.lua new file mode 100644 index 0000000..5c9f2d7 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/job/validation_spec.lua @@ -0,0 +1,28 @@ +local Job = require "plenary.job" + +describe("Job Validation", function() + it("does not require command when called with array method", function() + local ok, j = pcall(Job.new, Job, { "ls" }) + assert(ok, "Accepts positional arguments") + assert(j.command == "ls") + end) + + it("cannot use command and array syntax", function() + local ok = pcall(Job.new, Job, { "ls", command = "ls" }) + assert(not ok, "cannot use command and array syntax") + end) + + it("can parse command and args from array syntax", function() + local ok, j = pcall(Job.new, Job, { "ls", "-al" }) + assert(ok, "Accepts positional arguments") + assert(j.command == "ls") + assert.are.same({ "-al" }, j.args) + end) + + it("can parse command and multiple args from array syntax", function() + local ok, j = pcall(Job.new, Job, { "ls", "-al", "~" }) + assert(ok, "Accepts positional arguments") + assert(j.command == "ls") + assert.are.same({ "-al", "~" }, j.args) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/job_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/job_spec.lua new file mode 100644 index 0000000..c2edde0 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/job_spec.lua @@ -0,0 +1,864 @@ +local Job = require "plenary.job" + +local has_all_executables = function(execs) + for _, e in ipairs(execs) do + if vim.fn.executable(e) == 0 then + return false + end + end + + return true +end + +local tables_equal = function(t1, t2) + if #t1 ~= #t2 then + return false + end + for i, t1_v in ipairs(t1) do + if t2[i] ~= t1_v then + return false + end + end + return true +end + +local wait_for_result = function(job, result) + if type(result) == "string" then + result = { result } + end + vim.wait(1000, function() + return tables_equal(job:result(), result) + end) +end + +describe("Job", function() + describe("> cat manually >", function() + it("should split simple stdin", function() + local results = {} + local job = Job:new { + command = "cat", + + on_stdout = function(_, data) + table.insert(results, data) + end, + } + + job:start() + job:send "hello\n" + job:send "world\n" + + wait_for_result(job, { "hello", "world" }) + job:shutdown() + + assert.are.same(job:result(), { "hello", "world" }) + assert.are.same(job:result(), results) + end) + + it("should allow empty strings", function() + local results = {} + local job = Job:new { + command = "cat", + + on_stdout = function(_, data) + table.insert(results, data) + end, + } + + job:start() + job:send "hello\n" + job:send "\n" + job:send "world\n" + job:send "\n" + + wait_for_result(job, { "hello", "", "world", "" }) + job:shutdown() + + assert.are.same(job:result(), { "hello", "", "world", "" }) + assert.are.same(job:result(), results) + end) + + it("should split stdin across newlines", function() + local results = {} + local job = Job:new { + -- writer = "hello\nword\nthis is\ntj", + command = "cat", + + on_stdout = function(_, data) + table.insert(results, data) + end, + } + + job:start() + job:send "hello\nwor" + job:send "ld\n" + + wait_for_result(job, { "hello", "world" }) + job:shutdown() + + assert.are.same(job:result(), { "hello", "world" }) + assert.are.same(job:result(), results) + end) + + it("should split stdin across newlines with no ending newline", function() + local results = {} + local job = Job:new { + -- writer = "hello\nword\nthis is\ntj", + command = "cat", + + on_stdout = function(_, data) + table.insert(results, data) + end, + } + + job:start() + job:send "hello\nwor" + job:send "ld" + + wait_for_result(job, { "hello", "world" }) + job:shutdown() + + assert.are.same(job:result(), { "hello", "world" }) + assert.are.same(job:result(), results) + end) + + it("should return last line when there is ending newline", function() + local results = {} + local job = Job:new { + command = "printf", + + args = { "test1\ntest2\n" }, + + on_stdout = function(_, data) + table.insert(results, data) + end, + } + + job:sync() + + assert.are.same(job:result(), { "test1", "test2" }) + assert.are.same(job:result(), results) + end) + + it("should return last line when there is no ending newline", function() + local results = {} + local job = Job:new { + command = "printf", + + args = { "test1\ntest2" }, + + on_stdout = function(_, data) + table.insert(results, data) + end, + } + + job:sync() + + assert.are.same(job:result(), { "test1", "test2" }) + assert.are.same(job:result(), results) + end) + end) + + describe("env", function() + it("should be possible to set one env variable with an array", function() + local results = {} + local job = Job:new { + command = "env", + env = { "A=100" }, + on_stdout = function(_, data) + table.insert(results, data) + end, + } + + job:sync() + + assert.are.same(job:result(), { "A=100" }) + assert.are.same(job:result(), results) + end) + + it("should be possible to set multiple env variables with an array", function() + local results = {} + local job = Job:new { + command = "env", + env = { "A=100", "B=test" }, + on_stdout = function(_, data) + table.insert(results, data) + end, + } + + job:sync() + + assert.are.same(job:result(), { "A=100", "B=test" }) + assert.are.same(job:result(), results) + end) + + it("should be possible to set one env variable with a map", function() + local results = {} + local job = Job:new { + command = "env", + env = { "A=100" }, + on_stdout = function(_, data) + table.insert(results, data) + end, + } + + job:sync() + + assert.are.same(job:result(), { "A=100" }) + assert.are.same(job:result(), results) + end) + + it("should be possible to set one env variable with spaces", function() + local results = {} + local job = Job:new { + command = "env", + env = { "A=This is a long env var" }, + on_stdout = function(_, data) + table.insert(results, data) + end, + } + + job:sync() + + assert.are.same(job:result(), { "A=This is a long env var" }) + assert.are.same(job:result(), results) + end) + + it("should be possible to set one env variable with spaces and a map", function() + local results = {} + local job = Job:new { + command = "env", + env = { ["A"] = "This is a long env var" }, + on_stdout = function(_, data) + table.insert(results, data) + end, + } + + job:sync() + + assert.are.same(job:result(), { "A=This is a long env var" }) + assert.are.same(job:result(), results) + end) + + it("should be possible to set multiple env variables with a map", function() + local results = {} + local job = Job:new { + command = "env", + env = { ["A"] = 100, ["B"] = "test" }, + on_stdout = function(_, data) + table.insert(results, data) + end, + } + + job:sync() + + local expected = { "A=100", "B=test" } + local found = { false, false } + for k, v in ipairs(job:result()) do + for _, w in ipairs(expected) do + if v == w then + found[k] = true + end + end + end + + assert.are.same({ true, true }, found) + assert.are.same(job:result(), results) + end) + + it("should be possible to set multiple env variables with both, array and map", function() + local results = {} + local job = Job:new { + command = "env", + env = { ["A"] = 100, "B=test" }, + on_stdout = function(_, data) + table.insert(results, data) + end, + } + + job:sync() + + local expected = { "A=100", "B=test" } + local found = { false, false } + for k, v in ipairs(job:result()) do + for _, w in ipairs(expected) do + if v == w then + found[k] = true + end + end + end + + assert.are.same({ true, true }, found) + assert.are.same(job:result(), results) + end) + end) + + describe("> simple ls >", function() + it("should match systemlist", function() + local ls_results = vim.fn.systemlist "ls -l" + + local job = Job:new { + command = "ls", + args = { "-l" }, + } + + job:sync() + assert.are.same(job:result(), ls_results) + end) + + it("should match larger systemlist", function() + local results = vim.fn.systemlist "find ." + local stdout_results = {} + + local job = Job:new { + command = "find", + args = { "." }, + + on_stdout = function(_, line) + table.insert(stdout_results, line) + end, + } + + job:sync() + assert.are.same(job:result(), results) + assert.are.same(job:result(), stdout_results) + end) + + it("should not timeout when completing fast jobs", function() + local start = vim.loop.hrtime() + + local job = Job:new { command = "ls" } + + job:sync() + + assert((vim.loop.hrtime() - start) / 1e9 < 1, "Should not take one second to complete") + end) + + it("should return the return code as well", function() + local job = Job:new { command = "false" } + local _, ret = job:sync() + + assert.are.same(1, job.code) + assert.are.same(1, ret) + end) + end) + + describe("chain", function() + it("should always run the next job when using and_then", function() + local results = {} + + local first_job = Job:new { + command = "env", + env = { ["a"] = "1" }, + on_stdout = function(_, line) + table.insert(results, line) + end, + } + + local second_job = Job:new { + command = "env", + env = { ["b"] = "2" }, + on_stdout = function(_, line) + table.insert(results, line) + end, + } + + local third_job = Job:new { command = "false" } + + local fourth_job = Job:new { + command = "env", + env = { ["c"] = "3" }, + on_stdout = function(_, line) + table.insert(results, line) + end, + } + + first_job:and_then(second_job) + second_job:and_then(third_job) + third_job:and_then(fourth_job) + + first_job:sync() + second_job:wait() + third_job:wait() + fourth_job:wait() + + assert.are.same({ "a=1", "b=2", "c=3" }, results) + assert.are.same({ "a=1" }, first_job:result()) + assert.are.same({ "b=2" }, second_job:result()) + assert.are.same(1, third_job.code) + assert.are.same({ "c=3" }, fourth_job:result()) + end) + + it("should only run the next job on success when using and_then_on_success", function() + local results = {} + + local first_job = Job:new { + command = "env", + env = { ["a"] = "1" }, + on_stdout = function(_, line) + table.insert(results, line) + end, + } + + local second_job = Job:new { + command = "env", + env = { ["b"] = "2" }, + on_stdout = function(_, line) + table.insert(results, line) + end, + } + + local third_job = Job:new { command = "false" } + + local fourth_job = Job:new { + command = "env", + env = { ["c"] = "3" }, + on_stdout = function(_, line) + table.insert(results, line) + end, + } + + first_job:and_then_on_success(second_job) + second_job:and_then_on_success(third_job) + third_job:and_then_on_success(fourth_job) + + first_job:sync() + second_job:wait() + third_job:wait() + + assert.are.same({ "a=1", "b=2" }, results) + assert.are.same({ "a=1" }, first_job:result()) + assert.are.same({ "b=2" }, second_job:result()) + assert.are.same(1, third_job.code) + assert.are.same(nil, fourth_job.handle, "Job never started") + end) + + it("should only run the next job on failure when using and_then_on_failure", function() + local results = {} + + local first_job = Job:new { + command = "false", + } + + local second_job = Job:new { + command = "env", + env = { ["a"] = "1" }, + on_stdout = function(_, line) + table.insert(results, line) + end, + } + + local third_job = Job:new { + command = "env", + env = { ["b"] = "2" }, + on_stdout = function(_, line) + table.insert(results, line) + end, + } + + first_job:and_then_on_failure(second_job) + second_job:and_then_on_failure(third_job) + + local _, ret = first_job:sync() + second_job:wait() + + assert.are.same(1, first_job.code) + assert.are.same(1, ret) + assert.are.same({ "a=1" }, results) + assert.are.same({ "a=1" }, second_job:result()) + assert.are.same(nil, third_job.handle, "Job never started") + end) + + it("should run all normal functions when using after", function() + local results = {} + local code = 0 + + local first_job = Job:new { + command = "env", + env = { ["a"] = "1" }, + on_stdout = function(_, line) + table.insert(results, line) + end, + } + + local second_job = Job:new { command = "false" } + + first_job + :after(function() + code = code + 10 + end) + :and_then(second_job) + second_job:after(function(_, c) + code = code + c + end) + + first_job:sync() + second_job:wait() + + assert.are.same({ "a=1" }, results) + assert.are.same({ "a=1" }, first_job:result()) + assert.are.same(1, second_job.code) + assert.are.same(11, code) + end) + + it("should run only on success normal functions when using after_success", function() + local results = {} + local code = 0 + + local first_job = Job:new { + command = "env", + env = { ["a"] = "1" }, + on_stdout = function(_, line) + table.insert(results, line) + end, + } + + local second_job = Job:new { command = "false" } + local third_job = Job:new { command = "true" } + + first_job:after_success(function() + code = code + 10 + end) + first_job:and_then_on_success(second_job) + second_job:after_success(function(_, c) + code = code + c + end) + second_job:and_then_on_success(third_job) + + first_job:sync() + second_job:wait() + + assert.are.same({ "a=1" }, results) + assert.are.same({ "a=1" }, first_job:result()) + assert.are.same(1, second_job.code) + assert.are.same(10, code) + assert.are.same(nil, third_job.handle) + end) + + it("should run only on failure normal functions when using after_failure", function() + local results = {} + local code = 0 + + local first_job = Job:new { command = "false" } + + local second_job = Job:new { + command = "env", + env = { ["a"] = "1" }, + on_stdout = function(_, line) + table.insert(results, line) + end, + } + + local third_job = Job:new { command = "true" } + + first_job:after_failure(function(_, c) + code = code + c + end) + first_job:and_then_on_failure(second_job) + second_job:after_failure(function() + code = code + 10 + end) + second_job:and_then_on_failure(third_job) + + local _, ret = first_job:sync() + second_job:wait() + + assert.are.same({ "a=1" }, results) + assert.are.same({ "a=1" }, second_job:result()) + assert.are.same(1, ret) + assert.are.same(1, first_job.code) + assert.are.same(1, code) + assert.are.same(0, second_job.code) + assert.are.same(nil, third_job.handle) + end) + end) + + describe(".writer", function() + pending("should allow using things like fzf", function() + if not has_all_executables { "fzf", "fdfind" } then + return + end + + local stdout_results = {} + + local fzf = Job:new { + writer = Job:new { + command = "fdfind", + cwd = vim.fn.expand "~/plugins/plenary.nvim/", + }, + + command = "fzf", + args = { "--filter", "job.lua" }, + + cwd = vim.fn.expand "~/plugins/plenary.nvim/", + + on_stdout = function(_, line) + table.insert(stdout_results, line) + end, + } + + local results = fzf:sync() + assert.are.same(results, stdout_results) + + -- 'job.lua' should be the best file from fzf. + -- So make sure we're processing correctly. + assert.are.same("lua/plenary/job.lua", results[1]) + end) + + it("should work with a table", function() + if not has_all_executables { "fzf" } then + return + end + + local stdout_results = {} + + local fzf = Job:new { + writer = { "hello", "world", "job.lua" }, + + command = "fzf", + args = { "--filter", "job.lua" }, + + on_stdout = function(_, line) + table.insert(stdout_results, line) + end, + } + + local results = fzf:sync() + assert.are.same(results, stdout_results) + + -- 'job.lua' should be the best file from fzf. + -- So make sure we're processing correctly. + assert.are.same("job.lua", results[1]) + assert.are.same(1, #results) + end) + + it("should work with a string", function() + if not has_all_executables { "fzf" } then + return + end + + local stdout_results = {} + + local fzf = Job:new { + writer = "hello\nworld\njob.lua", + + command = "fzf", + args = { "--filter", "job.lua" }, + + on_stdout = function(_, line) + table.insert(stdout_results, line) + end, + } + + local results = fzf:sync() + assert.are.same(results, stdout_results) + + -- 'job.lua' should be the best file from fzf. + -- So make sure we're processing correctly. + assert.are.same("job.lua", results[1]) + assert.are.same(1, #results) + end) + + it("should work with a pipe", function() + if not has_all_executables { "fzf" } then + return + end + + local input_pipe = vim.loop.new_pipe(false) + + local stdout_results = {} + local fzf = Job:new { + writer = input_pipe, + + command = "fzf", + args = { "--filter", "job.lua" }, + + on_stdout = function(_, line) + table.insert(stdout_results, line) + end, + } + + fzf:start() + + input_pipe:write "hello\n" + input_pipe:write "world\n" + input_pipe:write "job.lua\n" + input_pipe:close() + + wait_for_result(fzf, "job.lua") + fzf:shutdown() + + local results = fzf:result() + assert.are.same(results, stdout_results) + + -- 'job.lua' should be the best file from fzf. + -- So make sure we're processing correctly. + assert.are.same("job.lua", results[1]) + assert.are.same(1, #results) + end) + + it("should work with a pipe, but no final newline", function() + if not has_all_executables { "fzf" } then + return + end + + local input_pipe = vim.loop.new_pipe(false) + + local stdout_results = {} + local fzf = Job:new { + writer = input_pipe, + + command = "fzf", + args = { "--filter", "job.lua" }, + + on_stdout = function(_, line) + table.insert(stdout_results, line) + end, + } + + fzf:start() + + input_pipe:write "hello\n" + input_pipe:write "world\n" + input_pipe:write "job.lua" + input_pipe:close() + + wait_for_result(fzf, "job.lua") + fzf:shutdown() + + local results = fzf:result() + assert.are.same(results, stdout_results) + + -- 'job.lua' should be the best file from fzf. + -- So make sure we're processing correctly. + assert.are.same("job.lua", results[1]) + assert.are.same(1, #results) + end) + end) + + describe(":wait()", function() + it("should respect timeout", function() + local j = Job:new { + command = "sleep", + args = { "10" }, + } + + local ok = pcall(j.sync, j, 500) + assert(not ok, "Job should fail") + end) + end) + + describe("enable_.*", function() + it("should not add things to results when disabled", function() + local job = Job:new { + command = "ls", + args = { "-l" }, + + enable_recording = false, + } + + local res = job:sync() + assert(res == nil, "No results should exist") + assert(job._stdout_results == nil, "No result table") + end) + + it("should not call callbacks when disabled", function() + local was_called = false + local job = Job:new { + command = "ls", + args = { "-l" }, + + enable_handlers = false, + + on_stdout = function() + was_called = true + end, + } + + job:sync() + assert(not was_called, "Should not be called.") + assert(job._stdout_results == nil, "No result table") + end) + end) + + describe("enable_.*", function() + it("should not add things to results when disabled", function() + local job = Job:new { + command = "ls", + args = { "-l" }, + + enable_recording = false, + } + + local res = job:sync() + assert(res == nil, "No results should exist") + assert(job._stdout_results == nil, "No result table") + end) + + it("should not call callbacks when disbaled", function() + local was_called = false + local job = Job:new { + command = "ls", + args = { "-l" }, + + enable_handlers = false, + + on_stdout = function() + was_called = true + end, + } + + job:sync() + assert(not was_called, "Should not be called.") + assert(job._stdout_results == nil, "No result table") + end) + end) + + describe("validation", function() + it("requires options", function() + local ok = pcall(Job.new, { command = "ls" }) + assert(not ok, "Requires options") + end) + + it("requires command", function() + local ok = pcall(Job.new, Job, { cmd = "ls" }) + assert(not ok, "Requires command") + end) + + it("will not spawn jobs with invalid commands", function() + local ok = pcall(Job.new, Job, { command = "dasowlwl" }) + assert(not ok, "Should not allow invalid executables") + end) + end) + + describe("on_exit", function() + it("should only be called once for wait", function() + local count = 0 + + local job = Job:new { + command = "ls", + on_exit = function(...) + count = count + 1 + end, + } + job:start() + job:wait() + + assert.are.same(count, 1) + end) + + it("should only be called once for shutdown", function() + local count = 0 + + local job = Job:new { + command = "ls", + on_exit = function(...) + count = count + 1 + end, + } + job:start() + job:shutdown() + + assert.are.same(count, 1) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/json_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/json_spec.lua new file mode 100644 index 0000000..88c00c2 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/json_spec.lua @@ -0,0 +1,87 @@ +local json = require "plenary.json" +local eq = assert.are.same + +describe("json", function() + it("replace comments with whitespace", function() + eq(json.json_strip_comments '//comment\n{"a":"b"}', ' \n{"a":"b"}') + eq(json.json_strip_comments '/*//comment*/{"a":"b"}', ' {"a":"b"}') + eq(json.json_strip_comments '{"a":"b"//comment\n}', '{"a":"b" \n}') + eq(json.json_strip_comments '{"a":"b"/*comment*/}', '{"a":"b" }') + eq(json.json_strip_comments '{"a"/*\n\n\ncomment\r\n*/:"b"}', '{"a" \n\n\n \r\n :"b"}') + eq(json.json_strip_comments '/*!\n * comment\n */\n{"a":"b"}', ' \n \n \n{"a":"b"}') + eq(json.json_strip_comments '{/*comment*/"a":"b"}', '{ "a":"b"}') + end) + + it("remove comments", function() + local options = { whitespace = false } + eq(json.json_strip_comments('//comment\n{"a":"b"}', options), '\n{"a":"b"}') + eq(json.json_strip_comments('/*//comment*/{"a":"b"}', options), '{"a":"b"}') + eq(json.json_strip_comments('{"a":"b"//comment\n}', options), '{"a":"b"\n}') + eq(json.json_strip_comments('{"a":"b"/*comment*/}', options), '{"a":"b"}') + eq(json.json_strip_comments('{"a"/*\n\n\ncomment\r\n*/:"b"}', options), '{"a":"b"}') + eq(json.json_strip_comments('/*!\n * comment\n */\n{"a":"b"}', options), '\n{"a":"b"}') + eq(json.json_strip_comments('{/*comment*/"a":"b"}', options), '{"a":"b"}') + end) + + it("doesn't strip comments inside strings", function() + eq(json.json_strip_comments '{"a":"b//c"}', '{"a":"b//c"}') + eq(json.json_strip_comments '{"a":"b/*c*/"}', '{"a":"b/*c*/"}') + eq(json.json_strip_comments '{"/*a":"b"}', '{"/*a":"b"}') + eq(json.json_strip_comments '{"\\"/*a":"b"}', '{"\\"/*a":"b"}') + end) + + it("consider escaped slashes when checking for escaped string quote", function() + eq(json.json_strip_comments '{"\\\\":"https://foobar.com"}', '{"\\\\":"https://foobar.com"}') + eq(json.json_strip_comments '{"foo\\"":"https://foobar.com"}', '{"foo\\"":"https://foobar.com"}') + end) + + it("line endings - no comments", function() + eq(json.json_strip_comments '{"a":"b"\n}', '{"a":"b"\n}') + eq(json.json_strip_comments '{"a":"b"\r\n}', '{"a":"b"\r\n}') + end) + + it("line endings - single line comment", function() + eq(json.json_strip_comments '{"a":"b"//c\n}', '{"a":"b" \n}') + eq(json.json_strip_comments '{"a":"b"//c\r\n}', '{"a":"b" \r\n}') + end) + + it("line endings - single line block comment", function() + eq(json.json_strip_comments '{"a":"b"/*c*/\n}', '{"a":"b" \n}') + eq(json.json_strip_comments '{"a":"b"/*c*/\r\n}', '{"a":"b" \r\n}') + end) + + it("line endings - multi line block comment", function() + eq(json.json_strip_comments '{"a":"b",/*c\nc2*/"x":"y"\n}', '{"a":"b", \n "x":"y"\n}') + eq(json.json_strip_comments '{"a":"b",/*c\r\nc2*/"x":"y"\r\n}', '{"a":"b", \r\n "x":"y"\r\n}') + end) + + it("line endings - works at EOF", function() + local options = { whitespace = false } + eq(json.json_strip_comments '{\r\n\t"a":"b"\r\n} //EOF', '{\r\n\t"a":"b"\r\n} ') + eq(json.json_strip_comments('{\r\n\t"a":"b"\r\n} //EOF', options), '{\r\n\t"a":"b"\r\n} ') + end) + + it("handles weird escaping", function() + eq( + json.json_strip_comments [[{"x":"x \"sed -e \\\"s/^.\\\\{46\\\\}T//\\\" -e \\\"s/#033/\\\\x1b/g\\\"\""}]], + [[{"x":"x \"sed -e \\\"s/^.\\\\{46\\\\}T//\\\" -e \\\"s/#033/\\\\x1b/g\\\"\""}]] + ) + end) + + it("trailing commas", function() + eq(json.json_strip_comments '{"a":"b",}', '{"a":"b"}') + eq(json.json_strip_comments '{"a":{"b":"c",},}', '{"a":{"b":"c"}}') + eq(json.json_strip_comments '{"a":["b","c",],}', '{"a":["b","c"]}') + end) + + it("trailing commas - ignored in strings and comments", function() + eq(json.json_strip_comments '{"a":"b,}"}', '{"a":"b,}"}') + end) + + it("trailing commas - left when disabled in options", function() + local options = { trailing_commas = true } + eq(json.json_strip_comments('{"a":"b",}', options), '{"a":"b",}') + eq(json.json_strip_comments('{"a":{"b":"c",},}', options), '{"a":{"b":"c",},}') + eq(json.json_strip_comments('{"a":["b","c",],}', options), '{"a":["b","c",],}') + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/path_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/path_spec.lua new file mode 100644 index 0000000..0f02db0 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/path_spec.lua @@ -0,0 +1,762 @@ +local Path = require "plenary.path" +local path = Path.path +local compat = require "plenary.compat" + +describe("Path", function() + it("should find valid files", function() + local p = Path:new "README.md" + assert(p.filename == "README.md", p.filename) + assert.are.same(p.filename, "README.md") + end) + + describe("absolute", function() + it(".absolute()", function() + local p = Path:new { "README.md", sep = "/" } + assert.are.same(p:absolute(), vim.fn.fnamemodify("README.md", ":p")) + end) + + it("can determine absolute paths", function() + local p = Path:new { "/home/asdfasdf/", sep = "/" } + assert(p:is_absolute(), "Is absolute") + assert(p:absolute() == p.filename) + end) + + it("can determine non absolute paths", function() + local p = Path:new { "./home/tj/", sep = "/" } + assert(not p:is_absolute(), "Is absolute") + end) + + it("will normalize the path", function() + local p = Path:new { "lua", "..", "README.md", sep = "/" } + assert.are.same(p:absolute(), vim.fn.fnamemodify("README.md", ":p")) + end) + end) + + it("can join paths by constructor or join path", function() + assert.are.same(Path:new("lua", "plenary"), Path:new("lua"):joinpath "plenary") + end) + + it("can join paths with /", function() + assert.are.same(Path:new("lua", "plenary"), Path:new "lua" / "plenary") + end) + + it("can join paths with paths", function() + assert.are.same(Path:new("lua", "plenary"), Path:new("lua", Path:new "plenary")) + end) + + it("inserts slashes", function() + assert.are.same("lua" .. path.sep .. "plenary", Path:new("lua", "plenary").filename) + end) + + describe(".exists()", function() + it("finds files that exist", function() + assert.are.same(true, Path:new("README.md"):exists()) + end) + + it("returns false for files that do not exist", function() + assert.are.same(false, Path:new("asdf.md"):exists()) + end) + end) + + describe(".is_dir()", function() + it("should find directories that exist", function() + assert.are.same(true, Path:new("lua"):is_dir()) + end) + + it("should return false when the directory does not exist", function() + assert.are.same(false, Path:new("asdf"):is_dir()) + end) + + it("should not show files as directories", function() + assert.are.same(false, Path:new("README.md"):is_dir()) + end) + end) + + describe(".is_file()", function() + it("should not allow directories", function() + assert.are.same(true, not Path:new("lua"):is_file()) + end) + + it("should return false when the file does not exist", function() + assert.are.same(true, not Path:new("asdf"):is_file()) + end) + + it("should show files as file", function() + assert.are.same(true, Path:new("README.md"):is_file()) + end) + end) + + describe(":new", function() + it("can be called with or without colon", function() + -- This will work, cause we used a colon + local with_colon = Path:new "lua" + local no_colon = Path.new "lua" + + assert.are.same(with_colon, no_colon) + end) + end) + + describe(":make_relative", function() + it("can take absolute paths and make them relative to the cwd", function() + local p = Path:new { "lua", "plenary", "path.lua" } + local absolute = vim.loop.cwd() .. path.sep .. p.filename + local relative = Path:new(absolute):make_relative() + assert.are.same(relative, p.filename) + end) + + it("can take absolute paths and make them relative to a given path", function() + local root = path.sep == "\\" and "c:\\" or "/" + local r = Path:new { root, "home", "prime" } + local p = Path:new { "aoeu", "agen.lua" } + local absolute = r.filename .. path.sep .. p.filename + local relative = Path:new(absolute):make_relative(r.filename) + assert.are.same(relative, p.filename) + end) + + it("can take double separator absolute paths and make them relative to the cwd", function() + local p = Path:new { "lua", "plenary", "path.lua" } + local absolute = vim.loop.cwd() .. path.sep .. path.sep .. p.filename + local relative = Path:new(absolute):make_relative() + assert.are.same(relative, p.filename) + end) + + it("can take double separator absolute paths and make them relative to a given path", function() + local root = path.sep == "\\" and "c:\\" or "/" + local r = Path:new { root, "home", "prime" } + local p = Path:new { "aoeu", "agen.lua" } + local absolute = r.filename .. path.sep .. path.sep .. p.filename + local relative = Path:new(absolute):make_relative(r.filename) + assert.are.same(relative, p.filename) + end) + + it("can take absolute paths and make them relative to a given path with trailing separator", function() + local root = path.sep == "\\" and "c:\\" or "/" + local r = Path:new { root, "home", "prime" } + local p = Path:new { "aoeu", "agen.lua" } + local absolute = r.filename .. path.sep .. p.filename + local relative = Path:new(absolute):make_relative(r.filename .. path.sep) + assert.are.same(relative, p.filename) + end) + + it("can take absolute paths and make them relative to the root directory", function() + local root = path.sep == "\\" and "c:\\" or "/" + local p = Path:new { "home", "prime", "aoeu", "agen.lua" } + local absolute = root .. p.filename + local relative = Path:new(absolute):make_relative(root) + assert.are.same(relative, p.filename) + end) + + it("can take absolute paths and make them relative to themselves", function() + local root = path.sep == "\\" and "c:\\" or "/" + local p = Path:new { root, "home", "prime", "aoeu", "agen.lua" } + local relative = Path:new(p.filename):make_relative(p.filename) + assert.are.same(relative, ".") + end) + + it("should not truncate if path separator is not present after cwd", function() + local cwd = "tmp" .. path.sep .. "foo" + local p = Path:new { "tmp", "foo_bar", "fileb.lua" } + local relative = Path:new(p.filename):make_relative(cwd) + assert.are.same(p.filename, relative) + end) + + it("should not truncate if path separator is not present after cwd and cwd ends in path sep", function() + local cwd = "tmp" .. path.sep .. "foo" .. path.sep + local p = Path:new { "tmp", "foo_bar", "fileb.lua" } + local relative = Path:new(p.filename):make_relative(cwd) + assert.are.same(p.filename, relative) + end) + end) + + describe(":normalize", function() + it("can take path that has one character directories", function() + local orig = "/home/j/./p//path.lua" + local final = Path:new(orig):normalize() + assert.are.same(final, "/home/j/p/path.lua") + end) + + it("can take paths with double separators change them to single separators", function() + local orig = "/lua//plenary/path.lua" + local final = Path:new(orig):normalize() + assert.are.same(final, "/lua/plenary/path.lua") + end) + -- this may be redundant since normalize just calls make_relative which is tested above + it("can take absolute paths with double seps" .. "and make them relative with single seps", function() + local orig = "/lua//plenary/path.lua" + local final = Path:new(orig):normalize() + assert.are.same(final, "/lua/plenary/path.lua") + end) + + it("can remove the .. in paths", function() + local orig = "/lua//plenary/path.lua/foo/bar/../.." + local final = Path:new(orig):normalize() + assert.are.same(final, "/lua/plenary/path.lua") + end) + + it("can normalize relative paths", function() + assert.are.same(Path:new("lua/plenary/path.lua"):normalize(), "lua/plenary/path.lua") + end) + + it("can normalize relative paths containing ..", function() + assert.are.same(Path:new("lua/plenary/path.lua/../path.lua"):normalize(), "lua/plenary/path.lua") + end) + + it("can normalize relative paths with initial ..", function() + local p = Path:new "../lua/plenary/path.lua" + p._cwd = "/tmp/lua" + assert.are.same("lua/plenary/path.lua", p:normalize()) + end) + + it("can normalize relative paths to absolute when initial .. count matches cwd parts", function() + local p = Path:new "../../tmp/lua/plenary/path.lua" + p._cwd = "/tmp/lua" + assert.are.same("/tmp/lua/plenary/path.lua", p:normalize()) + end) + + it("can normalize ~ when file is within home directory (trailing slash)", function() + local home = "/home/test/" + local p = Path:new { home, "./test_file" } + p.path.home = home + p._cwd = "/tmp/lua" + assert.are.same("~/test_file", p:normalize()) + end) + + it("can normalize ~ when file is within home directory (no trailing slash)", function() + local home = "/home/test" + local p = Path:new { home, "./test_file" } + p.path.home = home + p._cwd = "/tmp/lua" + assert.are.same("~/test_file", p:normalize()) + end) + + it("handles usernames with a dash at the end", function() + local home = "/home/mattr-" + local p = Path:new { home, "test_file" } + p.path.home = home + p._cwd = "/tmp/lua" + assert.are.same("~/test_file", p:normalize()) + end) + + it("handles filenames with the same prefix as the home directory", function() + local p = Path:new "/home/test.old/test_file" + p.path.home = "/home/test" + assert.are.same("/home/test.old/test_file", p:normalize()) + end) + end) + + describe(":shorten", function() + it("can shorten a path", function() + local long_path = "/this/is/a/long/path" + local short_path = Path:new(long_path):shorten() + assert.are.same(short_path, "/t/i/a/l/path") + end) + + it("can shorten a path's components to a given length", function() + local long_path = "/this/is/a/long/path" + local short_path = Path:new(long_path):shorten(2) + assert.are.same(short_path, "/th/is/a/lo/path") + + -- without the leading / + long_path = "this/is/a/long/path" + short_path = Path:new(long_path):shorten(3) + assert.are.same(short_path, "thi/is/a/lon/path") + + -- where len is greater than the length of the final component + long_path = "this/is/an/extremely/long/path" + short_path = Path:new(long_path):shorten(5) + assert.are.same(short_path, "this/is/an/extre/long/path") + end) + + it("can shorten a path's components when excluding parts", function() + local long_path = "/this/is/a/long/path" + local short_path = Path:new(long_path):shorten(nil, { 1, -1 }) + assert.are.same(short_path, "/this/i/a/l/path") + + -- without the leading / + long_path = "this/is/a/long/path" + short_path = Path:new(long_path):shorten(nil, { 1, -1 }) + assert.are.same(short_path, "this/i/a/l/path") + + -- where excluding positions greater than the number of parts + long_path = "this/is/an/extremely/long/path" + short_path = Path:new(long_path):shorten(nil, { 2, 4, 6, 8 }) + assert.are.same(short_path, "t/is/a/extremely/l/path") + + -- where excluding positions less than the negation of the number of parts + long_path = "this/is/an/extremely/long/path" + short_path = Path:new(long_path):shorten(nil, { -2, -4, -6, -8 }) + assert.are.same(short_path, "this/i/an/e/long/p") + end) + + it("can shorten a path's components to a given length and exclude positions", function() + local long_path = "/this/is/a/long/path" + local short_path = Path:new(long_path):shorten(2, { 1, -1 }) + assert.are.same(short_path, "/this/is/a/lo/path") + + long_path = "this/is/a/long/path" + short_path = Path:new(long_path):shorten(3, { 2, -2 }) + assert.are.same(short_path, "thi/is/a/long/pat") + + long_path = "this/is/an/extremely/long/path" + short_path = Path:new(long_path):shorten(5, { 3, -3 }) + assert.are.same(short_path, "this/is/an/extremely/long/path") + end) + end) + + describe("mkdir / rmdir", function() + it("can create and delete directories", function() + local p = Path:new "_dir_not_exist" + + p:rmdir() + assert(not p:exists(), "After rmdir, it should not exist") + + p:mkdir() + assert(p:exists()) + + p:rmdir() + assert(not p:exists()) + end) + + it("fails when exists_ok is false", function() + local p = Path:new "lua" + assert(not pcall(p.mkdir, p, { exists_ok = false })) + end) + + it("fails when parents is not passed", function() + local p = Path:new("impossible", "dir") + assert(not pcall(p.mkdir, p, { parents = false })) + assert(not p:exists()) + end) + + it("can create nested directories", function() + local p = Path:new("impossible", "dir") + assert(pcall(p.mkdir, p, { parents = true })) + assert(p:exists()) + + p:rmdir() + Path:new("impossible"):rmdir() + assert(not p:exists()) + assert(not Path:new("impossible"):exists()) + end) + end) + + describe("touch", function() + it("can create and delete new files", function() + local p = Path:new "test_file.lua" + assert(pcall(p.touch, p)) + assert(p:exists()) + + p:rm() + assert(not p:exists()) + end) + + it("does not effect already created files but updates last access", function() + local p = Path:new "README.md" + local last_atime = p:_stat().atime.sec + local last_mtime = p:_stat().mtime.sec + + local lines = p:readlines() + + assert(pcall(p.touch, p)) + print(p:_stat().atime.sec > last_atime) + print(p:_stat().mtime.sec > last_mtime) + assert(p:exists()) + + assert.are.same(lines, p:readlines()) + end) + + it("does not create dirs if nested in none existing dirs and parents not set", function() + local p = Path:new { "nested", "nested2", "test_file.lua" } + assert(not pcall(p.touch, p, { parents = false })) + assert(not p:exists()) + end) + + it("does create dirs if nested in none existing dirs", function() + local p1 = Path:new { "nested", "nested2", "test_file.lua" } + local p2 = Path:new { "nested", "asdf", ".hidden" } + local d1 = Path:new { "nested", "dir", ".hidden" } + assert(pcall(p1.touch, p1, { parents = true })) + assert(pcall(p2.touch, p2, { parents = true })) + assert(pcall(d1.mkdir, d1, { parents = true })) + assert(p1:exists()) + assert(p2:exists()) + assert(d1:exists()) + + Path:new({ "nested" }):rm { recursive = true } + assert(not p1:exists()) + assert(not p2:exists()) + assert(not d1:exists()) + assert(not Path:new({ "nested" }):exists()) + end) + end) + + describe("rename", function() + it("can rename a file", function() + local p = Path:new "a_random_filename.lua" + assert(pcall(p.touch, p)) + assert(p:exists()) + + assert(pcall(p.rename, p, { new_name = "not_a_random_filename.lua" })) + assert.are.same("not_a_random_filename.lua", p.filename) + + p:rm() + end) + + it("can handle an invalid filename", function() + local p = Path:new "some_random_filename.lua" + assert(pcall(p.touch, p)) + assert(p:exists()) + + assert(not pcall(p.rename, p, { new_name = "" })) + assert(not pcall(p.rename, p)) + assert.are.same("some_random_filename.lua", p.filename) + + p:rm() + end) + + it("can move to parent dir", function() + local p = Path:new "some_random_filename.lua" + assert(pcall(p.touch, p)) + assert(p:exists()) + + assert(pcall(p.rename, p, { new_name = "../some_random_filename.lua" })) + assert.are.same(vim.loop.fs_realpath(Path:new("../some_random_filename.lua"):absolute()), p:absolute()) + + p:rm() + end) + + it("cannot rename to an existing filename", function() + local p1 = Path:new "a_random_filename.lua" + local p2 = Path:new "not_a_random_filename.lua" + assert(pcall(p1.touch, p1)) + assert(pcall(p2.touch, p2)) + assert(p1:exists()) + assert(p2:exists()) + + assert(not pcall(p1.rename, p1, { new_name = "not_a_random_filename.lua" })) + assert.are.same(p1.filename, "a_random_filename.lua") + + p1:rm() + p2:rm() + end) + end) + + describe("copy", function() + it("can copy a file", function() + local p1 = Path:new "a_random_filename.rs" + local p2 = Path:new "not_a_random_filename.rs" + assert(pcall(p1.touch, p1)) + assert(p1:exists()) + + assert(pcall(p1.copy, p1, { destination = "not_a_random_filename.rs" })) + assert.are.same(p1.filename, "a_random_filename.rs") + assert.are.same(p2.filename, "not_a_random_filename.rs") + + p1:rm() + p2:rm() + end) + + it("can copy to parent dir", function() + local p = Path:new "some_random_filename.lua" + assert(pcall(p.touch, p)) + assert(p:exists()) + + assert(pcall(p.copy, p, { destination = "../some_random_filename.lua" })) + assert(pcall(p.exists, p)) + + p:rm() + Path:new(vim.loop.fs_realpath "../some_random_filename.lua"):rm() + end) + + it("cannot copy an existing file if override false", function() + local p1 = Path:new "a_random_filename.rs" + local p2 = Path:new "not_a_random_filename.rs" + assert(pcall(p1.touch, p1)) + assert(pcall(p2.touch, p2)) + assert(p1:exists()) + assert(p2:exists()) + + assert(pcall(p1.copy, p1, { destination = "not_a_random_filename.rs", override = false })) + assert.are.same(p1.filename, "a_random_filename.rs") + assert.are.same(p2.filename, "not_a_random_filename.rs") + + p1:rm() + p2:rm() + end) + + it("fails when copying folders non-recursively", function() + local src_dir = Path:new "src" + src_dir:mkdir() + src_dir:joinpath("file1.lua"):touch() + + local trg_dir = Path:new "trg" + local status = xpcall(function() + src_dir:copy { destination = trg_dir, recursive = false } + end, function() end) + -- failed as intended + assert(status == false) + + src_dir:rm { recursive = true } + end) + + it("can copy directories recursively", function() + -- vim.tbl_flatten doesn't work here as copy doesn't return a list + local flatten + flatten = function(ret, t) + for _, v in pairs(t) do + if type(v) == "table" then + flatten(ret, v) + else + table.insert(ret, v) + end + end + end + + -- setup directories + local src_dir = Path:new "src" + local trg_dir = Path:new "trg" + src_dir:mkdir() + + -- set up sub directory paths for creation and testing + local sub_dirs = { "sub_dir1", "sub_dir1/sub_dir2" } + local src_dirs = { src_dir } + local trg_dirs = { trg_dir } + -- {src, trg}_dirs is a table with all directory levels by {src, trg} + for _, dir in ipairs(sub_dirs) do + table.insert(src_dirs, src_dir:joinpath(dir)) + table.insert(trg_dirs, trg_dir:joinpath(dir)) + end + + -- generate {file}_{level}.lua on every directory level in src + -- src + -- ├── file1_1.lua + -- ├── file2_1.lua + -- ├── .file3_1.lua + -- └── sub_dir1 + -- ├── file1_2.lua + -- ├── file2_2.lua + -- ├── .file3_2.lua + -- └── sub_dir2 + -- ├── file1_3.lua + -- ├── file2_3.lua + -- └── .file3_3.lua + local files = { "file1", "file2", ".file3" } + for _, file in ipairs(files) do + for level, dir in ipairs(src_dirs) do + local p = dir:joinpath(file .. "_" .. level .. ".lua") + assert(pcall(p.touch, p, { parents = true, exists_ok = true })) + assert(p:exists()) + end + end + + for _, hidden in ipairs { true, false } do + -- override = `false` should NOT copy as it was copied beforehand + for _, override in ipairs { true, false } do + local success = src_dir:copy { destination = trg_dir, recursive = true, override = override, hidden = hidden } + -- the files are already created because we iterate first with `override=true` + -- hence, we test here that no file ops have been committed: any value in tbl of tbls should be false + if not override then + local file_ops = {} + flatten(file_ops, success) + -- 3 layers with at at least 2 and at most 3 files (`hidden = true`) + local num_files = not hidden and 6 or 9 + assert(#file_ops == num_files) + for _, op in ipairs(file_ops) do + assert(op == false) + end + else + for _, file in ipairs(files) do + for level, dir in ipairs(trg_dirs) do + local p = dir:joinpath(file .. "_" .. level .. ".lua") + -- file 3 is hidden + if not (file == files[3]) then + assert(p:exists()) + else + assert(p:exists() == hidden) + end + end + end + end + -- only clean up once we tested that we dont want to copy + -- if `override=true` + if not override then + trg_dir:rm { recursive = true } + end + end + end + + src_dir:rm { recursive = true } + end) + end) + + describe("parents", function() + it("should extract the ancestors of the path", function() + local p = Path:new(vim.loop.cwd()) + local parents = p:parents() + assert(compat.islist(parents)) + for _, parent in pairs(parents) do + assert.are.same(type(parent), "string") + end + end) + it("should return itself if it corresponds to path.root", function() + local p = Path:new(Path.path.root(vim.loop.cwd())) + assert.are.same(p:parent(), p) + end) + end) + + describe("read parts", function() + it("should read head of file", function() + local p = Path:new "LICENSE" + local data = p:head() + local should = [[MIT License + +Copyright (c) 2020 TJ DeVries + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:]] + assert.are.same(should, data) + end) + + it("should read the first line of file", function() + local p = Path:new "LICENSE" + local data = p:head(1) + local should = [[MIT License]] + assert.are.same(should, data) + end) + + it("head should max read whole file", function() + local p = Path:new "LICENSE" + local data = p:head(1000) + local should = [[MIT License + +Copyright (c) 2020 TJ DeVries + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.]] + assert.are.same(should, data) + end) + + it("should read tail of file", function() + local p = Path:new "LICENSE" + local data = p:tail() + local should = [[The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.]] + assert.are.same(should, data) + end) + + it("should read the last line of file", function() + local p = Path:new "LICENSE" + local data = p:tail(1) + local should = [[SOFTWARE.]] + assert.are.same(should, data) + end) + + it("tail should max read whole file", function() + local p = Path:new "LICENSE" + local data = p:tail(1000) + local should = [[MIT License + +Copyright (c) 2020 TJ DeVries + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.]] + assert.are.same(should, data) + end) + end) + + describe("readbyterange", function() + it("should read bytes at given offset", function() + local p = Path:new "LICENSE" + local data = p:readbyterange(13, 10) + local should = "Copyright " + assert.are.same(should, data) + end) + + it("supports negative offset", function() + local p = Path:new "LICENSE" + local data = p:readbyterange(-10, 10) + local should = "SOFTWARE.\n" + assert.are.same(should, data) + end) + end) + + describe(":find_upwards", function() + it("finds files that exist", function() + local p = Path:new(debug.getinfo(1, "S").source:sub(2)) + local found = p:find_upwards "README.md" + assert.are.same(found:absolute(), Path:new("README.md"):absolute()) + end) + + it("finds files that exist at the root", function() + local p = Path:new(debug.getinfo(1, "S").source:sub(2)) + + -- Temporarily set path.root to the root of this repository + local root = p.path.root + p.path.root = function(_) + return p:parent():parent():parent().filename + end + + local found = p:find_upwards "README.md" + assert.are.same(found:absolute(), Path:new("README.md"):absolute()) + p.path.root = root + end) + + it("returns nil if no file is found", function() + local p = Path:new(debug.getinfo(1, "S").source:sub(2)) + local found = p:find_upwards "MISSINGNO.md" + assert.are.same(found, nil) + end) + end) +end) + +-- function TestPath:testIsDir() +-- end + +-- function TestPath:testCanBeCalledWithoutColon() +-- end + +-- -- @sideeffect +-- function TestPath:testMkdir() +-- end diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/popup_requires_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/popup_requires_spec.lua new file mode 100644 index 0000000..2909f65 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/popup_requires_spec.lua @@ -0,0 +1,42 @@ +local scandir = require "plenary.scandir" +local Path = require "plenary.path" + +local eq = assert.are.same + +describe("plenary.popup", function() + local allowed_imports = { + ["plenary.window"] = true, + ["plenary.window.border"] = true, + } + + local matches_any_import = function(line) + local matched = string.match(line, [[require."(.*)"]]) + if matched and not vim.startswith(matched, "plenary.popup") then + if not allowed_imports[matched] then + return true, string.format("Not an allowed import for popup: %s. Line: %s", matched, line) + end + end + + return false, nil + end + + -- Tests to make sure that we're matching both types of requires + it("should match these kinds of patterns", function() + eq(true, matches_any_import [[local x = require "plenary.other"]]) + eq(true, matches_any_import [[local x = require("plenary.module").something]]) + end) + + it("must not require anything other than Window and Border from plenary", function() + local result = scandir.scan_dir("./lua/plenary/popup", { depth = 1 }) + + for _, file in ipairs(result) do + local popup_file = Path:new(file) + local lines = popup_file:readlines() + + for _, line in ipairs(lines) do + local matches, msg = matches_any_import(line) + eq(false, matches, msg) + end + end + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/popup_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/popup_spec.lua new file mode 100644 index 0000000..250ba0e --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/popup_spec.lua @@ -0,0 +1,162 @@ +local popup = require "plenary.popup" + +local eq = assert.are.same + +describe("plenary.popup", function() + before_each(function() + vim.cmd [[highlight PopupColor1 ctermbg=lightblue guibg=lightblue]] + vim.cmd [[highlight PopupColor2 ctermbg=lightcyan guibg=lightcyan]] + end) + + -- TODO: Probably want to clear all the popups between iterations + -- after_each(function() end) + + it("can create a very simple window", function() + local win_id = popup.create("hello there", { + line = 1, + col = 1, + width = 20, + }) + + local win_config = vim.api.nvim_win_get_config(win_id) + eq(20, win_config.width) + end) + + it("can create a very simple window after 'set nomodifiable'", function() + vim.o.modifiable = false + local win_id = popup.create("hello there", { + line = 1, + col = 1, + width = 20, + }) + + local win_config = vim.api.nvim_win_get_config(win_id) + eq(20, win_config.width) + vim.o.modifiable = true + end) + + it("can apply a highlight", function() + local win_id = popup.create("hello there", { + highlight = "PopupColor1", + }) + + eq("Normal:PopupColor1,EndOfBuffer:PopupColor1", vim.api.nvim_win_get_option(win_id, "winhl")) + end) + + it("can create a border", function() + local win_id, config = popup.create("hello border", { + line = 2, + col = 3, + border = {}, + }) + + eq(true, vim.api.nvim_win_is_valid(win_id)) + + local border_id = config.border.win_id + assert(border_id, "Has a border win id") + eq(true, vim.api.nvim_win_is_valid(border_id)) + end) + + it("can apply a border highlight", function() + local _, opts = popup.create("hello there", { + border = true, + borderhighlight = "PopupColor2", + }) + + local border_win_id = opts.border.win_id + eq("Normal:PopupColor2", vim.api.nvim_win_get_option(border_win_id, "winhl")) + end) + + it("can ignore border highlight with no border", function() + local _ = popup.create("hello there", { + border = false, + borderhighlight = "PopupColor3", + }) + end) + + it("can do basic padding", function() + local win_id = popup.create("12345", { + line = 1, + col = 1, + padding = {}, + }) + + local bufnr = vim.api.nvim_win_get_buf(win_id) + eq({ "", " 12345 ", "" }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) + end) + + it("can do padding and border", function() + local win_id, config = popup.create("hello border", { + line = 2, + col = 2, + border = {}, + padding = {}, + }) + + local bufnr = vim.api.nvim_win_get_buf(win_id) + eq({ "", " hello border ", "" }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) + + local border_id = config.border.win_id + assert(border_id, "Has a border win id") + eq(true, vim.api.nvim_win_is_valid(border_id)) + end) + + describe("borderchars", function() + local test_border = function(name, borderchars, expected) + it(name, function() + local _, config = popup.create("all the plus signs", { + line = 8, + col = 55, + padding = { 0, 3, 0, 3 }, + borderchars = borderchars, + }) + + local border_id = config.border.win_id + local border_bufnr = vim.api.nvim_win_get_buf(border_id) + eq(expected, vim.api.nvim_buf_get_lines(border_bufnr, 0, -1, false)) + end) + end + + test_border("can support multiple border patterns", { "+" }, { + "++++++++++++++++++++++++++", + "+ +", + "++++++++++++++++++++++++++", + }) + + test_border("can support multiple patterns inside the borderchars", { "-", "+" }, { + "+------------------------+", + "- -", + "+------------------------+", + }) + end) + + describe("what", function() + it("can be an existing bufnr", function() + local bufnr = vim.api.nvim_create_buf(false, false) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { "pass bufnr 1", "pass bufnr 2" }) + local win_id = popup.create(bufnr, { + line = 8, + col = 55, + minwidth = 20, + }) + + eq(bufnr, vim.api.nvim_win_get_buf(win_id)) + eq({ "pass bufnr 1", "pass bufnr 2" }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) + end) + end) + + describe("cursor", function() + pending("not yet tested", function() + popup.create({ "option 1", "option 2" }, { + line = "cursor+2", + col = "cursor+2", + border = { 1, 1, 1, 1 }, + enter = true, + cursorline = true, + callback = function(win_id, sel) + print(sel) + end, + }) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/py_list_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/py_list_spec.lua new file mode 100644 index 0000000..1ca48bf --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/py_list_spec.lua @@ -0,0 +1,60 @@ +local List = require "plenary.collections.py_list" +local Iter = require "plenary.iterators" + +describe("List", function() + it("should detect whether a value is an instance of List", function() + local l = List { 1, 2 } + local n = 42 + assert(List.is_list(l)) + assert(not List.is_list(n)) + end) + it("should be equal if all elements are equal", function() + local l1 = List { 1, 2, 3 } + local l2 = List { 1, 2, 3 } + local l3 = List { 4, 5, 6 } + assert.are.equal(l1, l2) + assert.are_not.equal(l1, l3) + end) + it("can be concatenated to other list-like tables", function() + local l1 = List { 1, 2, 3 } .. { 4 } + assert.are.equal(l1, List { 1, 2, 3, 4 }) + end) + it("can create a copy of itself with equal elements", function() + local l1 = List { 1, 2, 3 } + local l2 = l1:copy() + assert.are.equal(l1, l2) + end) + it("can create a slice between two indices", function() + local l1 = List { 1, 2, 3, 4 } + local l2 = l1:slice(2, 4) + assert.are.equal(l2, List { 2, 3, 4 }) + end) + it("can reverse itself in place", function() + local l = List { 1, 2, 3, 4 } + l:reverse() + assert.are.equal(l, List { 4, 3, 2, 1 }) + end) + it("can push elements to itself", function() + local l = List { 1, 2, 3 } + l:push(4) + assert.are.equal(l, List { 1, 2, 3, 4 }) + end) + it("can pop the n-th element from itself (last one by default)", function() + local l = List { 1, 2, 3, 4 } + local n = l:pop() + assert.are.equal(l, List { 1, 2, 3 }) + assert.are.equal(n, 4) + end) + it("can create a list from an iterable", function() + local l = List.from_iter(Iter.range(5, 10)) + assert.are.equal(l, List { 5, 6, 7, 8, 9, 10 }) + end) + it("can be partitioned based on a predicate", function() + local l = List.from_iter(Iter.range(1, 10)) + local evens, odds = l:iter():partition(function(e) + return e % 2 == 0 + end) + assert.are.equal(evens, List { 2, 4, 6, 8, 10 }) + assert.are.equal(odds, List { 1, 3, 5, 7, 9 }) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/rotate_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/rotate_spec.lua new file mode 100644 index 0000000..825228b --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/rotate_spec.lua @@ -0,0 +1,33 @@ +local rotate = require("plenary.vararg").rotate + +local eq = function(a, b) + assert.is["true"](vim.deep_equal(a, b), true) +end + +describe("rotate", function() + it("should return as many values, as the first argument", function() + local args = {} + for _ = 0, 20 do + local n = select("#", unpack(args)) + assert.is.equal(n, select("#", rotate(n, unpack(args)))) + args[#args + 1] = n + end + end) + + it("should rotate varargs", function() + eq({ rotate(3, 1, 2, 3) }, { 2, 3, 1 }) + eq({ rotate(9, 1, 2, 3, 4, 5, 6, 7, 8, 9) }, { 2, 3, 4, 5, 6, 7, 8, 9, 1 }) + end) + + it("should rotate zero", function() + assert.is.equal(0, select("#", rotate(0))) + end) + + it("should rotate none", function() + assert.is.equal(0, select("#", rotate())) + end) + + it("should rotate one", function() + eq({ rotate(1, 1) }, { 1 }) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/scandir_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/scandir_spec.lua new file mode 100644 index 0000000..9c15808 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/scandir_spec.lua @@ -0,0 +1,249 @@ +local scan = require "plenary.scandir" +local mock = require "luassert.mock" +local stub = require "luassert.stub" +local eq = assert.are.same + +local contains = function(tbl, str) + for _, v in ipairs(tbl) do + if v == str then + return true + end + end + return false +end + +local contains_match = function(tbl, str) + for _, v in ipairs(tbl) do + if v:match(str) then + return true + end + end + return false +end + +describe("scandir", function() + describe("can list all files recursive", function() + it("with cwd", function() + local dirs = scan.scan_dir "." + eq("table", type(dirs)) + eq(true, contains(dirs, "./README.md")) + eq(true, contains(dirs, "./LICENSE")) + eq(true, contains(dirs, "./lua/plenary/job.lua")) + eq(false, contains(dirs, "./asdf/asdf/adsf.lua")) + end) + + it("and callback gets called for each entry", function() + local count = 0 + local dirs = scan.scan_dir(".", { + on_insert = function() + count = count + 1 + end, + }) + eq("table", type(dirs)) + eq(true, contains(dirs, "./README.md")) + eq(true, contains(dirs, "./LICENSE")) + eq(true, contains(dirs, "./lua/plenary/job.lua")) + eq(false, contains(dirs, "./asdf/asdf/adsf.lua")) + eq(count, #dirs) + end) + + it("with multiple paths", function() + local dirs = scan.scan_dir { "./lua", "./tests" } + eq("table", type(dirs)) + eq(true, contains(dirs, "./lua/say.lua")) + eq(true, contains(dirs, "./lua/plenary/job.lua")) + eq(true, contains(dirs, "./tests/plenary/scandir_spec.lua")) + eq(false, contains(dirs, "./asdf/asdf/adsf.lua")) + end) + + it("with hidden files", function() + local dirs = scan.scan_dir(".", { hidden = true }) + eq("table", type(dirs)) + eq(true, contains(dirs, "./README.md")) + eq(true, contains(dirs, "./lua/plenary/job.lua")) + eq(true, contains(dirs, "./.gitignore")) + eq(false, contains(dirs, "./asdf/asdf/adsf.lua")) + end) + + it("with add directories", function() + local dirs = scan.scan_dir(".", { add_dirs = true }) + eq("table", type(dirs)) + eq(true, contains(dirs, "./README.md")) + eq(true, contains(dirs, "./lua/plenary/job.lua")) + eq(true, contains(dirs, "./lua")) + eq(true, contains(dirs, "./tests")) + eq(false, contains(dirs, "./asdf/asdf/adsf.lua")) + end) + + it("with only directories", function() + local dirs = scan.scan_dir(".", { only_dirs = true }) + eq("table", type(dirs)) + eq(false, contains(dirs, "./README.md")) + eq(false, contains(dirs, "./lua/plenary/job.lua")) + eq(true, contains(dirs, "./lua")) + eq(true, contains(dirs, "./tests")) + eq(false, contains(dirs, "./asdf/asdf/adsf.lua")) + end) + + it("until depth 1 is reached", function() + local dirs = scan.scan_dir(".", { depth = 1 }) + eq("table", type(dirs)) + eq(true, contains(dirs, "./README.md")) + eq(false, contains(dirs, "./lua")) + eq(false, contains(dirs, "./lua/say.lua")) + eq(false, contains(dirs, "./lua/plenary/job.lua")) + eq(false, contains(dirs, "./asdf/asdf/adsf.lua")) + end) + + it("until depth 1 is reached and with directories", function() + local dirs = scan.scan_dir(".", { depth = 1, add_dirs = true }) + eq("table", type(dirs)) + eq(true, contains(dirs, "./README.md")) + eq(true, contains(dirs, "./lua")) + eq(false, contains(dirs, "./lua/say.lua")) + eq(false, contains(dirs, "./lua/plenary/job.lua")) + eq(false, contains(dirs, "./asdf/asdf/adsf.lua")) + end) + + it("until depth 2 is reached", function() + local dirs = scan.scan_dir(".", { depth = 2 }) + eq("table", type(dirs)) + eq(true, contains(dirs, "./README.md")) + eq(true, contains(dirs, "./lua/say.lua")) + eq(false, contains(dirs, "./lua/plenary/job.lua")) + eq(false, contains(dirs, "./asdf/asdf/adsf.lua")) + end) + + it("with respect_gitignore", function() + vim.cmd ":silent !touch lua/test.so" + local dirs = scan.scan_dir(".", { respect_gitignore = true }) + vim.cmd ":silent !rm lua/test.so" + eq("table", type(dirs)) + eq(true, contains(dirs, "./README.md")) + eq(true, contains(dirs, "./LICENSE")) + eq(true, contains(dirs, "./lua/plenary/job.lua")) + eq(false, contains(dirs, "./lua/test.so")) + eq(false, contains(dirs, "./asdf/asdf/adsf.lua")) + end) + + it("with search pattern", function() + local dirs = scan.scan_dir(".", { search_pattern = "filetype" }) + eq("table", type(dirs)) + eq(true, contains(dirs, "./scripts/update_filetypes_from_github.lua")) + eq(true, contains(dirs, "./lua/plenary/filetype.lua")) + eq(true, contains(dirs, "./tests/plenary/filetype_spec.lua")) + eq(true, contains(dirs, "./data/plenary/filetypes/base.lua")) + eq(true, contains(dirs, "./data/plenary/filetypes/builtin.lua")) + eq(false, contains(dirs, "./README.md")) + end) + + it("with callback search pattern", function() + local dirs = scan.scan_dir(".", { + search_pattern = function(entry) + return entry:match "filetype" + end, + }) + eq("table", type(dirs)) + eq(true, contains(dirs, "./scripts/update_filetypes_from_github.lua")) + eq(true, contains(dirs, "./lua/plenary/filetype.lua")) + eq(true, contains(dirs, "./tests/plenary/filetype_spec.lua")) + eq(true, contains(dirs, "./data/plenary/filetypes/base.lua")) + eq(true, contains(dirs, "./data/plenary/filetypes/builtin.lua")) + eq(false, contains(dirs, "./README.md")) + end) + end) + + describe("gitignore", function() + local Path = require "plenary.path" + local mock_path, mock_gitignore + before_each(function() + mock_path = { + exists = stub.new().returns(true), + iter = function() + local i = 0 + local n = table.getn(mock_gitignore) + return function() + i = i + 1 + if i <= n then + return mock_gitignore[i] + end + end + end, + } + Path.new = stub.new().returns(mock_path) + end) + after_each(function() + Path.new:revert() + end) + + describe("ignores path", function() + it("when path matches pattern exactly", function() + mock_gitignore = { "ignored.txt" } + local should_add = scan.__make_gitignore { "path" } + eq(false, should_add({ "path" }, "./path/ignored.txt")) + end) + it("when path matches * pattern", function() + mock_gitignore = { "*.txt" } + local should_add = scan.__make_gitignore { "path" } + eq(false, should_add({ "path" }, "./path/dir/ignored.txt")) + end) + it("when path matches leading ** pattern", function() + mock_gitignore = { "**/ignored.txt" } + local should_add = scan.__make_gitignore { "path" } + eq(false, should_add({ "path" }, "./path/dir/subdir/ignored.txt")) + end) + it("when path matches trailing ** pattern", function() + mock_gitignore = { "/dir/**" } + local should_add = scan.__make_gitignore { "path" } + eq(false, should_add({ "path" }, "./path/dir/subdir/ignored.txt")) + end) + it("when path matches ? pattern", function() + mock_gitignore = { "ignore?.txt" } + local should_add = scan.__make_gitignore { "path" } + eq(false, should_add({ "path" }, "./path/ignored.txt")) + end) + end) + + describe("does not ignore path", function() + it("when path does not match", function() + mock_gitignore = { "ignored.txt" } + local should_add = scan.__make_gitignore { "path" } + eq(true, should_add({ "path" }, "./path/ok.txt")) + end) + it("when path is negated", function() + mock_gitignore = { "*.txt", "!ok.txt" } + local should_add = scan.__make_gitignore { "path" } + eq(true, should_add({ "path" }, "./path/ok.txt")) + end) + end) + end) + + describe("ls", function() + it("works for cwd", function() + local dirs = scan.ls "." + eq("table", type(dirs)) + eq(true, contains_match(dirs, "LICENSE")) + eq(true, contains_match(dirs, "README.md")) + eq(true, contains_match(dirs, "lua")) + eq(false, contains_match(dirs, "%.git$")) + end) + + it("works for another directory", function() + local dirs = scan.ls "./lua" + eq("table", type(dirs)) + eq(true, contains_match(dirs, "luassert")) + eq(true, contains_match(dirs, "plenary")) + eq(true, contains_match(dirs, "say.lua")) + eq(false, contains_match(dirs, "README.md")) + end) + + it("works with opts.hidden for cwd", function() + local dirs = scan.ls(".", { hidden = true }) + eq("table", type(dirs)) + eq(true, contains_match(dirs, "README.md")) + eq(true, contains_match(dirs, "LICENSE")) + eq(true, contains_match(dirs, "lua")) + eq(true, contains_match(dirs, "%.git$")) + end) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/simple_busted_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/simple_busted_spec.lua new file mode 100644 index 0000000..ce81ebf --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/simple_busted_spec.lua @@ -0,0 +1,206 @@ +local eq = assert.are.same + +local tester_function = function() + error(7) +end + +describe("busted specs", function() + describe("nested", function() + it("should work", function() + assert(true) + end) + end) + + it("should not nest", function() + assert(true) + end) + + it("should not fail unless we unpcall this", function() + pcall(tester_function) + end) + + pending("other thing pending", function() + error() + end) +end) + +describe("before each", function() + local a = 2 + local b = 3 + it("is not cleared", function() + eq(2, a) + eq(3, b) + a = a + 1 + b = b + 1 + end) + describe("nested", function() + before_each(function() + a = 0 + end) + it("should clear a but not b", function() + eq(0, a) + eq(4, b) + a = a + 1 + b = b + 1 + end) + describe("nested nested", function() + before_each(function() + b = 0 + end) + it("should clear b as well", function() + eq(0, a) + eq(0, b) + a = a + 1 + b = b + 1 + end) + end) + it("should only clear a", function() + eq(0, a) + eq(1, b) + a = a + 1 + b = b + 1 + end) + end) + it("should clear nothing", function() + eq(1, a) + eq(2, b) + end) +end) + +describe("before_each ordering", function() + local order = "" + before_each(function() + order = order .. "1," + end) + before_each(function() + order = order .. "2," + end) + describe("nested 1 deep", function() + before_each(function() + order = order .. "3," + end) + before_each(function() + order = order .. "4," + end) + describe("nested 2 deep", function() + before_each(function() + order = order .. "5," + end) + it("runs before_each`s in order", function() + eq("1,2,3,4,5,", order) + end) + end) + end) + describe("adjacent nested 1 deep", function() + before_each(function() + order = order .. "3a," + end) + before_each(function() + order = order .. "4a," + end) + describe("nested 2 deep", function() + before_each(function() + order = order .. "5a," + end) + it("runs before_each`s in order", function() + eq("1,2,3,4,5,1,2,3a,4a,5a,", order) + end) + end) + end) +end) + +describe("after each", function() + local a = 2 + local b = 3 + it("is not cleared", function() + eq(2, a) + eq(3, b) + a = a + 1 + b = b + 1 + end) + describe("nested", function() + after_each(function() + a = 0 + end) + it("should not clear any at this point", function() + eq(3, a) + eq(4, b) + a = a + 1 + b = b + 1 + end) + describe("nested nested", function() + after_each(function() + b = 0 + end) + it("should have cleared a", function() + eq(0, a) + eq(5, b) + a = a + 1 + b = b + 1 + end) + end) + it("should have cleared a and b", function() + eq(0, a) + eq(0, b) + a = a + 1 + b = b + 1 + end) + end) + it("should only have cleared a", function() + eq(0, a) + eq(1, b) + end) +end) + +describe("after_each ordering", function() + local order = "" + describe("1st describe having after_each", function() + after_each(function() + order = order .. "1," + end) + after_each(function() + order = order .. "2," + end) + describe("nested 1 deep", function() + after_each(function() + order = order .. "3," + end) + after_each(function() + order = order .. "4," + end) + describe("nested 2 deep", function() + after_each(function() + order = order .. "5," + end) + it("a test to trigger the after_each`s", function() + assert(true) + end) + end) + end) + describe("adjacent nested 1 deep", function() + after_each(function() + order = order .. "3a," + end) + after_each(function() + order = order .. "4a," + end) + describe("nested 2 deep", function() + after_each(function() + order = order .. "5a," + end) + it("a test to trigger the adjacent after_each`s", function() + assert(true) + end) + end) + end) + end) + it("ran after_each`s in order", function() + eq("1,2,3,4,5,1,2,3a,4a,5a,", order) + end) +end) + +describe("another top level describe test", function() + it("should work", function() + eq(1, 1) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/strings_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/strings_spec.lua new file mode 100644 index 0000000..d9abd6d --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/strings_spec.lua @@ -0,0 +1,305 @@ +local strings = require "plenary.strings" +local eq = assert.are.same + +describe("strings", function() + describe("strdisplaywidth", function() + for _, case in ipairs { + { str = "abcde", expected = { single = 5, double = 5 } }, + -- This space below is a tab (U+0009) + { str = "abc de", expected = { single = 10, double = 10 } }, + { str = "アイウエオ", expected = { single = 10, double = 10 } }, + { str = "├─┤", expected = { single = 3, double = 6 } }, + { str = 123, expected = { single = 3, double = 3 } }, + } do + for _, ambiwidth in ipairs { "single", "double" } do + local item = type(case.str) == "string" and '"%s"' or "%s" + local msg = ("ambiwidth = %s, " .. item .. " -> %d"):format(ambiwidth, case.str, case.expected[ambiwidth]) + local original = vim.o.ambiwidth + vim.o.ambiwidth = ambiwidth + it("lua: " .. msg, function() + eq(case.expected[ambiwidth], strings.strdisplaywidth(case.str)) + end) + it("vim: " .. msg, function() + eq(case.expected[ambiwidth], vim.fn.strdisplaywidth(case.str)) + end) + vim.o.ambiwidth = original + end + end + end) + + describe("strcharpart", function() + for _, case in ipairs { + { args = { "abcde", 2 }, expected = "cde" }, + { args = { "abcde", 2, 2 }, expected = "cd" }, + { args = { "アイウエオ", 2, 2 }, expected = "ウエ" }, + { args = { "├───┤", 2, 2 }, expected = "──" }, + } do + local msg = ('("%s", %d, %s) -> "%s"'):format(case.args[1], case.args[2], tostring(case.args[3]), case.expected) + it("lua: " .. msg, function() + eq(case.expected, strings.strcharpart(unpack(case.args))) + end) + it("vim: " .. msg, function() + eq(case.expected, vim.fn.strcharpart(unpack(case.args))) + end) + end + end) + + describe("truncate", function() + for _, case in ipairs { + -- truncations from the right + { args = { "abcde", 6, nil, 1 }, expected = { single = "abcde", double = "abcde" } }, + { args = { "abcde", 5, nil, 1 }, expected = { single = "abcde", double = "abcde" } }, + { args = { "abcde", 4, nil, 1 }, expected = { single = "abc…", double = "ab…" } }, + { + args = { "アイウエオ", 11, nil, 1 }, + expected = { single = "アイウエオ", double = "アイウエオ" }, + }, + { + args = { "アイウエオ", 10, nil, 1 }, + expected = { single = "アイウエオ", double = "アイウエオ" }, + }, + { + args = { "アイウエオ", 9, nil, 1 }, + expected = { single = "アイウエ…", double = "アイウ…" }, + }, + { args = { "アイウエオ", 8, nil, 1 }, expected = { single = "アイウ…", double = "アイウ…" } }, + { args = { "├─┤", 7, nil, 1 }, expected = { single = "├─┤", double = "├─┤" } }, + { args = { "├─┤", 6, nil, 1 }, expected = { single = "├─┤", double = "├─┤" } }, + { args = { "├─┤", 5, nil, 1 }, expected = { single = "├─┤", double = "├…" } }, + { args = { "├─┤", 4, nil, 1 }, expected = { single = "├─┤", double = "├…" } }, + { args = { "├─┤", 3, nil, 1 }, expected = { single = "├─┤", double = "…" } }, + { args = { "├─┤", 2, nil, 1 }, expected = { single = "├…", double = "…" } }, + -- truncations from the left + { args = { "abcde", 6, nil, -1 }, expected = { single = "abcde", double = "abcde" } }, + { args = { "abcde", 5, nil, -1 }, expected = { single = "abcde", double = "abcde" } }, + { args = { "abcde", 4, nil, -1 }, expected = { single = "…cde", double = "…de" } }, + { + args = { "アイウエオ", 11, nil, -1 }, + expected = { single = "アイウエオ", double = "アイウエオ" }, + }, + { + args = { "アイウエオ", 10, nil, -1 }, + expected = { single = "アイウエオ", double = "アイウエオ" }, + }, + { + args = { "アイウエオ", 9, nil, -1 }, + expected = { single = "…イウエオ", double = "…ウエオ" }, + }, + { args = { "アイウエオ", 8, nil, -1 }, expected = { single = "…ウエオ", double = "…ウエオ" } }, + { args = { "├─┤", 7, nil, -1 }, expected = { single = "├─┤", double = "├─┤" } }, + { args = { "├─┤", 6, nil, -1 }, expected = { single = "├─┤", double = "├─┤" } }, + { args = { "├─┤", 5, nil, -1 }, expected = { single = "├─┤", double = "…┤" } }, + { args = { "├─┤", 4, nil, -1 }, expected = { single = "├─┤", double = "…┤" } }, + { args = { "├─┤", 3, nil, -1 }, expected = { single = "├─┤", double = "…" } }, + { args = { "├─┤", 2, nil, -1 }, expected = { single = "…┤", double = "…" } }, + -- truncations from the middle + { args = { "abcde", 6, nil, 0 }, expected = { single = "abcde", double = "abcde" } }, + { args = { "abcde", 5, nil, 0 }, expected = { single = "abcde", double = "abcde" } }, + { args = { "abcde", 4, nil, 0 }, expected = { single = "a…de", double = "a…e" } }, + { + args = { "アイウエオ", 11, nil, 0 }, + expected = { single = "アイウエオ", double = "アイウエオ" }, + }, + { + args = { "アイウエオ", 10, nil, 0 }, + expected = { single = "アイウエオ", double = "アイウエオ" }, + }, + { + args = { "アイウエオ", 9, nil, 0 }, + expected = { single = "アイ…エオ", double = "ア…エオ" }, + }, + { args = { "アイウエオ", 8, nil, 0 }, expected = { single = "ア…エオ", double = "ア…エオ" } }, + { args = { "├─┤", 7, nil, 0 }, expected = { single = "├─┤", double = "├─┤" } }, + { args = { "├─┤", 6, nil, 0 }, expected = { single = "├─┤", double = "├─┤" } }, + { args = { "├─┤", 5, nil, 0 }, expected = { single = "├─┤", double = "…┤" } }, + { args = { "├─┤", 4, nil, 0 }, expected = { single = "├─┤", double = "…┤" } }, + { args = { "├─┤", 3, nil, 0 }, expected = { single = "├─┤", double = "…" } }, + { args = { "├─┤", 2, nil, 0 }, expected = { single = "…┤", double = "…" } }, + } do + for _, ambiwidth in ipairs { "single", "double" } do + local msg = ("ambiwidth = %s, direction = %s, [%s, %d] -> %s"):format( + ambiwidth, + (case.args[4] > 0) and "right" or (case.args[4] < 0) and "left" or "middle", + case.args[1], + case.args[2], + case.expected[ambiwidth] + ) + it(msg, function() + local original = vim.o.ambiwidth + vim.o.ambiwidth = ambiwidth + eq(case.expected[ambiwidth], strings.truncate(unpack(case.args))) + vim.o.ambiwidth = original + end) + end + end + end) + + describe("align_str", function() + for _, case in ipairs { + { args = { "abcde", 8 }, expected = { single = "abcde ", double = "abcde " } }, + { args = { "アイウ", 8 }, expected = { single = "アイウ ", double = "アイウ " } }, + { args = { "├─┤", 8 }, expected = { single = "├─┤ ", double = "├─┤ " } }, + { args = { "abcde", 8, true }, expected = { single = " abcde", double = " abcde" } }, + { args = { "アイウ", 8, true }, expected = { single = " アイウ", double = " アイウ" } }, + { args = { "├─┤", 8, true }, expected = { single = " ├─┤", double = " ├─┤" } }, + } do + for _, ambiwidth in ipairs { "single", "double" } do + local msg = ('ambiwidth = %s, [%s, %d, %s] -> "%s"'):format( + ambiwidth, + case.args[1], + case.args[2], + tostring(case.args[3]), + case.expected[ambiwidth] + ) + it(msg, function() + local original = vim.o.ambiwidth + vim.o.ambiwidth = ambiwidth + eq(case.expected[ambiwidth], strings.align_str(unpack(case.args))) + vim.o.ambiwidth = original + end) + end + end + end) + + describe("dedent", function() + local function lines(t) + return table.concat(t, "\n") + end + for _, case in ipairs { + { + msg = "empty string", + tabstop = 8, + args = { "" }, + expected = "", + }, + { + msg = "in case tabs are longer than spaces", + tabstop = 8, + args = { + lines { + " -> 13 spaces", + " 5 spaces -> 0 space", + }, + }, + expected = lines { + " -> 13 spaces", + "5 spaces -> 0 space", + }, + }, + { + msg = "in case tabs are shorter than spaces", + tabstop = 2, + args = { + lines { + " -> 0 space", + " 5spaces -> 1 space", + }, + }, + expected = lines { + " -> 0 space", + " 5spaces -> 1 space", + }, + }, + { + msg = "ignores empty lines", + tabstop = 2, + args = { + lines { + "", + "", + "", + " 8 spaces -> 3 spaces", + "", + "", + " 5 spaces -> 0 space", + "", + "", + "", + }, + }, + expected = lines { + "", + "", + "", + " 8 spaces -> 3 spaces", + "", + "", + "5 spaces -> 0 space", + "", + "", + "", + }, + }, + { + msg = "no indent", + tabstop = 2, + args = { + lines { + " -> 2 spaces", + "Here is no indent.", + " 4 spaces will remain", + }, + }, + expected = lines { + " -> 2 spaces", + "Here is no indent.", + " 4 spaces will remain", + }, + }, + { + msg = "leave_indent = 4", + tabstop = 2, + args = { + lines { + " -> 6 spaces", + "0 indent -> 4 spaces", + " 4 spaces -> 8 spaces", + }, + 4, + }, + expected = lines { + " -> 6 spaces", + " 0 indent -> 4 spaces", + " 4 spaces -> 8 spaces", + }, + }, + { + msg = "typical usecase: to 5 spaces", + tabstop = 4, + args = { + lines { + "", + " Chapter 1", + "", + " Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed", + " do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "", + " Ut enim ad minim veniam, quis nostrud exercitation ullamco", + " laboris nisi ut aliquip ex ea commodo consequat.", + "", + }, + 5, + }, + expected = lines { + "", + " Chapter 1", + "", + " Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed", + " do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "", + " Ut enim ad minim veniam, quis nostrud exercitation ullamco", + " laboris nisi ut aliquip ex ea commodo consequat.", + "", + }, + }, + } do + local msg = ("tabstop = %d, %s"):format(case.tabstop, case.msg) + it(msg, function() + local original = vim.bo.tabstop + vim.bo.tabstop = case.tabstop + eq(case.expected, strings.dedent(unpack(case.args))) + vim.bo.tabstop = original + end) + end + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/tbl_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/tbl_spec.lua new file mode 100644 index 0000000..3b5a936 --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/tbl_spec.lua @@ -0,0 +1,27 @@ +local tbl = require "plenary.tbl" + +local function should_fail(fun) + local stat = pcall(fun) + assert(not stat, "Function should have errored") +end + +describe("tbl utilities", function() + it("should be able to freeze a table", function() + local t = { 1, 2, 3 } + local frozen = tbl.freeze(t) + assert(t[1] == frozen[1]) + assert(t[2] == frozen[2]) + assert(t[3] == frozen[3]) + + should_fail(function() + frozen[4] = "thisthis" + end) + + should_fail(function() + frozen.hello = "asdfasdf" + end) + + assert(not frozen[5]) + assert(not frozen.hello) + end) +end) diff --git a/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/uses_nvim_spec.lua b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/uses_nvim_spec.lua new file mode 100644 index 0000000..61e4c5d --- /dev/null +++ b/.config/nvim/pack/tree/start/plenary.nvim/tests/plenary/uses_nvim_spec.lua @@ -0,0 +1,5 @@ +describe("simple nvim test", function() + it("should work", function() + vim.cmd "let g:val = v:true" + end) +end) diff --git a/.local/share/fonts/0xProtoNerdFont-Bold.ttf b/.local/share/fonts/0xProtoNerdFont-Bold.ttf new file mode 100644 index 0000000..d4e8516 Binary files /dev/null and b/.local/share/fonts/0xProtoNerdFont-Bold.ttf differ diff --git a/.local/share/fonts/0xProtoNerdFont-Italic.ttf b/.local/share/fonts/0xProtoNerdFont-Italic.ttf new file mode 100644 index 0000000..90cf13a Binary files /dev/null and b/.local/share/fonts/0xProtoNerdFont-Italic.ttf differ diff --git a/.local/share/fonts/0xProtoNerdFont-Regular.ttf b/.local/share/fonts/0xProtoNerdFont-Regular.ttf new file mode 100644 index 0000000..7436940 Binary files /dev/null and b/.local/share/fonts/0xProtoNerdFont-Regular.ttf differ diff --git a/.local/share/fonts/0xProtoNerdFontMono-Bold.ttf b/.local/share/fonts/0xProtoNerdFontMono-Bold.ttf new file mode 100644 index 0000000..2229144 Binary files /dev/null and b/.local/share/fonts/0xProtoNerdFontMono-Bold.ttf differ diff --git a/.local/share/fonts/0xProtoNerdFontMono-Italic.ttf b/.local/share/fonts/0xProtoNerdFontMono-Italic.ttf new file mode 100644 index 0000000..215de2a Binary files /dev/null and b/.local/share/fonts/0xProtoNerdFontMono-Italic.ttf differ diff --git a/.local/share/fonts/0xProtoNerdFontMono-Regular.ttf b/.local/share/fonts/0xProtoNerdFontMono-Regular.ttf new file mode 100644 index 0000000..6094706 Binary files /dev/null and b/.local/share/fonts/0xProtoNerdFontMono-Regular.ttf differ diff --git a/.local/share/fonts/0xProtoNerdFontPropo-Bold.ttf b/.local/share/fonts/0xProtoNerdFontPropo-Bold.ttf new file mode 100644 index 0000000..7ca0053 Binary files /dev/null and b/.local/share/fonts/0xProtoNerdFontPropo-Bold.ttf differ diff --git a/.local/share/fonts/0xProtoNerdFontPropo-Italic.ttf b/.local/share/fonts/0xProtoNerdFontPropo-Italic.ttf new file mode 100644 index 0000000..19d22fa Binary files /dev/null and b/.local/share/fonts/0xProtoNerdFontPropo-Italic.ttf differ diff --git a/.local/share/fonts/0xProtoNerdFontPropo-Regular.ttf b/.local/share/fonts/0xProtoNerdFontPropo-Regular.ttf new file mode 100644 index 0000000..b42ef34 Binary files /dev/null and b/.local/share/fonts/0xProtoNerdFontPropo-Regular.ttf differ diff --git a/.local/share/fonts/LICENSE b/.local/share/fonts/LICENSE new file mode 100644 index 0000000..fefebe8 --- /dev/null +++ b/.local/share/fonts/LICENSE @@ -0,0 +1,92 @@ +Copyright (c) 2023, 0xType Project Authors (https://github.com/0xType) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/.local/share/fonts/README.md b/.local/share/fonts/README.md new file mode 100644 index 0000000..151fb06 --- /dev/null +++ b/.local/share/fonts/README.md @@ -0,0 +1,48 @@ +# Nerd Fonts + +This is an archived font from the Nerd Fonts release v3.4.0. + +For more information see: +* https://github.com/ryanoasis/nerd-fonts/ +* https://github.com/ryanoasis/nerd-fonts/releases/latest/ + +# 0xProto + +A programming font focused on source code legibility. + +For more information have a look at the upstream website: https://github.com/0xType/0xProto + +Version: 2.300 + +## Which font? + +### TL;DR + +* Pick your font family: + * If you are limited to monospaced fonts (because of your terminal, etc) then pick a font with `Nerd Font Mono` (or `NFM`). + * If you want to have bigger icons (usually around 1.5 normal letters wide) pick a font without `Mono` i.e. `Nerd Font` (or `NF`). Most terminals support this, but ymmv. + * If you work in a proportional context (GUI elements or edit a presentation etc) pick a font with `Nerd Font Propo` (or `NFP`). + +### Ligatures + +Ligatures are generally preserved in the patched fonts. +Nerd Fonts `v2.0.0` had no ligatures in the `Nerd Font Mono` fonts, this has been dropped with `v2.1.0`. +If you have a ligature-aware terminal and don't want ligatures you can (usually) disable them in the terminal settings. + +### Explanation + +Once you narrow down your font choice of family (`Droid Sans`, `Inconsolata`, etc) and style (`bold`, `italic`, etc) you have 2 main choices: + +#### `Option 1: Download already patched font` + + * For a stable version download a font package from the [release page](https://github.com/ryanoasis/nerd-fonts/releases) + * Direct links for [0xProto.zip](https://github.com/ryanoasis/nerd-fonts/releases/latest/download/0xProto.zip) or [0xProto.tar.xz](https://github.com/ryanoasis/nerd-fonts/releases/latest/download/0xProto.tar.xz) + +#### `Option 2: Patch your own font` + + * Patch your own variations with the various options provided by the font patcher (i.e. not include all symbols for smaller font size) + +For more information see: [The FAQ](https://github.com/ryanoasis/nerd-fonts/wiki/FAQ-and-Troubleshooting#which-font) + +[SIL-RFN]:http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web_fonts_and_RFNs#14cbfd4a +