diff options
| author | Mike Vink <mike@pionative.com> | 2025-01-19 13:52:31 +0100 |
|---|---|---|
| committer | Mike Vink <mike@pionative.com> | 2025-01-19 13:52:31 +0100 |
| commit | 25b8e552377190d115e1c1e11b831b0b803e0c59 (patch) | |
| tree | 3cded1ce48e583209cb775b7a2b52fe80ea377ff | |
| parent | eed645f093d4ee441b143e28d9cca514dcd4182e (diff) | |
| parent | c65afb488eb9eab85063d79783d40ae1d7138586 (diff) | |
Merge commit 'c65afb488eb9eab85063d79783d40ae1d7138586' as 'mut/neovim/pack/plugins/start/quicker.nvim'
78 files changed, 4773 insertions, 0 deletions
diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.envrc b/mut/neovim/pack/plugins/start/quicker.nvim/.envrc new file mode 100644 index 0000000..94b55e4 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.envrc @@ -0,0 +1,3 @@ +export VIRTUAL_ENV=venv +layout python +python -c 'import pyparsing' 2> /dev/null || pip install -r scripts/requirements.txt diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/ISSUE_TEMPLATE/bug_report.yml b/mut/neovim/pack/plugins/start/quicker.nvim/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..1fcb4d8 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,116 @@ +name: Bug Report +description: File a bug/issue +title: "bug: " +labels: [bug] +body: + - type: markdown + attributes: + value: | + Before reporting a bug, make sure to search [existing issues](https://github.com/stevearc/quicker.nvim/issues) + - type: input + attributes: + label: "Neovim version (nvim -v)" + placeholder: "0.10.0 commit db1b0ee3b30f" + 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. + validations: + required: true + - type: dropdown + attributes: + label: What is the severity of this bug? + options: + - minor (annoyance) + - tolerable (can work around it) + - breaking (some functionality is broken) + - blocking (cannot use plugin) + validations: + required: true + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. nvim -u repro.lua + 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: Minimal example file + description: A small example file you are editing that produces the issue + validations: + required: false + - type: textarea + attributes: + label: Minimal init.lua + description: + Minimal `init.lua` to reproduce this issue. Save as `repro.lua` and run with `nvim -u repro.lua` + This uses lazy.nvim (a plugin manager). + value: | + -- DO NOT change the paths and don't remove the colorscheme + local root = vim.fn.fnamemodify("./.repro", ":p") + + -- set stdpaths to use .repro + for _, name in ipairs({ "config", "data", "state", "cache" }) do + vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name + end + + -- bootstrap lazy + local lazypath = root .. "/plugins/lazy.nvim" + if not vim.loop.fs_stat(lazypath) then + vim.fn.system({ + "git", + "clone", + "--filter=blob:none", + "--single-branch", + "https://github.com/folke/lazy.nvim.git", + lazypath, + }) + end + vim.opt.runtimepath:prepend(lazypath) + + -- install plugins + local plugins = { + "folke/tokyonight.nvim", + { + "stevearc/quicker.nvim", + config = function() + require("quicker").setup({ + -- add your config here + }) + end, + }, + -- add any other plugins here + } + require("lazy").setup(plugins, { + root = root .. "/plugins", + }) + + vim.cmd.colorscheme("tokyonight") + -- add anything else here + render: Lua + validations: + required: false + - type: textarea + attributes: + label: Additional context + description: Any additional information or screenshots you would like to provide + validations: + required: false diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/ISSUE_TEMPLATE/feature_request.yml b/mut/neovim/pack/plugins/start/quicker.nvim/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..f735c8c --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,43 @@ +name: Feature Request +description: Submit a feature request +title: "feature request: " +labels: [enhancement] +body: + - type: markdown + attributes: + value: | + Before submitting a feature request, make sure to search for [existing requests](https://github.com/stevearc/quicker.nvim/issues) + - type: checkboxes + attributes: + label: Did you check existing requests? + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Describe the feature + description: A short summary of the feature you want + validations: + required: true + - type: textarea + attributes: + label: Provide background + description: Describe the reasoning behind why you want the feature. + placeholder: I am trying to do X. My current workflow is Y. + validations: + required: false + - type: dropdown + attributes: + label: What is the significance of this feature? + options: + - nice to have + - strongly desired + - cannot use this plugin without it + validations: + required: true + - type: textarea + attributes: + label: Additional details + description: Any additional information you would like to provide. Things you've tried, alternatives considered, examples from other plugins, etc. + validations: + required: false diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/pre-commit b/mut/neovim/pack/plugins/start/quicker.nvim/.github/pre-commit new file mode 100755 index 0000000..c64fbec --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/pre-commit @@ -0,0 +1,3 @@ +#!/bin/bash +set -e +make fastlint diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/pre-push b/mut/neovim/pack/plugins/start/quicker.nvim/.github/pre-push new file mode 100755 index 0000000..ecb23a9 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/pre-push @@ -0,0 +1,11 @@ +#!/bin/bash +set -e +IFS=' ' +while read local_ref _local_sha _remote_ref _remote_sha; do + remote_main=$( (git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo "///master") | cut -f 4 -d / | tr -d "[:space:]") + local_ref_short=$(echo "$local_ref" | cut -f 3 -d / | tr -d "[:space:]") + if [ "$local_ref_short" = "$remote_main" ]; then + make lint + make test + fi +done diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/automation_remove_question_label_on_comment.yml b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/automation_remove_question_label_on_comment.yml new file mode 100644 index 0000000..f99bba8 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/automation_remove_question_label_on_comment.yml @@ -0,0 +1,16 @@ +name: Remove Question Label on Issue Comment + +on: [issue_comment] + +jobs: + # Remove the "question" label when a new comment is added. + # This lets me ask a question, tag the issue with "question", and filter out all "question"-tagged + # issues in my "needs triage" filter. + remove_question: + runs-on: ubuntu-latest + if: github.event.sender.login != 'stevearc' + steps: + - uses: actions/checkout@v4 + - uses: actions-ecosystem/action-remove-labels@v1 + with: + labels: question diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/automation_request_review.yml b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/automation_request_review.yml new file mode 100644 index 0000000..c31f582 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/automation_request_review.yml @@ -0,0 +1,27 @@ +name: Request Review +permissions: + pull-requests: write +on: + pull_request_target: + types: [opened, reopened, ready_for_review, synchronize] + branches-ignore: + - "release-please--**" + +jobs: + # Request review automatically when PRs are opened + request_review: + runs-on: ubuntu-latest + steps: + - name: Request Review + uses: actions/github-script@v7 + if: github.actor != 'stevearc' + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const pr = context.payload.pull_request; + github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + reviewers: ['stevearc'] + }); diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/install_nvim.sh b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/install_nvim.sh new file mode 100644 index 0000000..4c0203c --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/install_nvim.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e +PLUGINS="$HOME/.local/share/nvim/site/pack/plugins/start" +mkdir -p "$PLUGINS" + +wget "https://github.com/neovim/neovim/releases/download/${NVIM_TAG-stable}/nvim.appimage" +chmod +x nvim.appimage +./nvim.appimage --appimage-extract >/dev/null +rm -f nvim.appimage +mkdir -p ~/.local/share/nvim +mv squashfs-root ~/.local/share/nvim/appimage +sudo ln -s "$HOME/.local/share/nvim/appimage/AppRun" /usr/bin/nvim diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/tests.yml b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/tests.yml new file mode 100644 index 0000000..a053f5d --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.github/workflows/tests.yml @@ -0,0 +1,122 @@ +name: Run tests + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + luacheck: + name: Luacheck + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Prepare + run: | + sudo apt-get update + sudo add-apt-repository universe + sudo apt install luarocks -y + sudo luarocks install luacheck + + - name: Run Luacheck + run: luacheck lua tests + + typecheck: + name: typecheck + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: stevearc/nvim-typecheck-action@v2 + with: + path: lua + + stylua: + name: StyLua + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Stylua + uses: JohnnyMorganz/stylua-action@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + version: v0.20.0 + args: --check lua tests + + run_tests: + strategy: + matrix: + include: + - nvim_tag: v0.10.1 + + name: Run tests + runs-on: ubuntu-22.04 + env: + NVIM_TAG: ${{ matrix.nvim_tag }} + steps: + - uses: actions/checkout@v4 + + - name: Install Neovim and dependencies + run: | + bash ./.github/workflows/install_nvim.sh + + - name: Run tests + run: | + bash ./run_tests.sh + + update_docs: + name: Update docs + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Install Neovim and dependencies + run: | + bash ./.github/workflows/install_nvim.sh + + - name: Update docs + run: | + python -m pip install pyparsing==3.0.9 + make doc + - name: Commit changes + if: ${{ github.ref == 'refs/heads/master' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMIT_MSG: | + [docgen] Update docs + skip-checks: true + run: | + git config user.email "actions@github" + git config user.name "Github Actions" + git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git + git add README.md doc + # Only commit and push if we have changes + git diff --quiet && git diff --staged --quiet || (git commit -m "${COMMIT_MSG}"; git push origin HEAD:${GITHUB_REF}) + + release: + name: release + + if: ${{ github.ref == 'refs/heads/master' }} + needs: + - luacheck + - stylua + - typecheck + - run_tests + - update_docs + runs-on: ubuntu-22.04 + steps: + - uses: googleapis/release-please-action@v4 + id: release + with: + release-type: simple + - uses: actions/checkout@v4 + - uses: rickstaa/action-create-tag@v1 + if: ${{ steps.release.outputs.release_created }} + with: + tag: stable + message: "Current stable release: ${{ steps.release.outputs.tag_name }}" + tag_exists_error: false + force_push_tag: true diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.gitignore b/mut/neovim/pack/plugins/start/quicker.nvim/.gitignore new file mode 100644 index 0000000..9e6bcf7 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.gitignore @@ -0,0 +1,48 @@ +# 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 + +.direnv/ +.testenv/ +venv/ +doc/tags +scripts/nvim_doc_tools +scripts/nvim-typecheck-action +tests/tmp diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.luacheckrc b/mut/neovim/pack/plugins/start/quicker.nvim/.luacheckrc new file mode 100644 index 0000000..5e100b1 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.luacheckrc @@ -0,0 +1,17 @@ +max_comment_line_length = false +codes = true + +exclude_files = { + "tests/", +} + +ignore = { + "212", -- Unused argument + "631", -- Line is too long + "122", -- Setting a readonly global + "542", -- Empty if branch +} + +read_globals = { + "vim", +} diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.luarc.json b/mut/neovim/pack/plugins/start/quicker.nvim/.luarc.json new file mode 100644 index 0000000..68da2f2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.luarc.json @@ -0,0 +1,9 @@ +{ + "runtime": { + "version": "LuaJIT", + "pathStrict": true + }, + "type": { + "checkTableShape": true + } +} diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/.stylua.toml b/mut/neovim/pack/plugins/start/quicker.nvim/.stylua.toml new file mode 100644 index 0000000..020ce91 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/.stylua.toml @@ -0,0 +1,5 @@ +column_width = 100 +indent_type = "Spaces" +indent_width = 2 +[sort_requires] +enabled = true diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/CHANGELOG.md b/mut/neovim/pack/plugins/start/quicker.nvim/CHANGELOG.md new file mode 100644 index 0000000..da1f1e5 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/CHANGELOG.md @@ -0,0 +1,65 @@ +# Changelog + +## [1.3.0](https://github.com/stevearc/quicker.nvim/compare/v1.2.0...v1.3.0) (2024-12-24) + + +### Features + +* add option to remove all leading whitespace from items ([#26](https://github.com/stevearc/quicker.nvim/issues/26)) ([da7e910](https://github.com/stevearc/quicker.nvim/commit/da7e9104de4ff9303e1c722f7c9216f994622067)) +* option to scroll to closest quickfix item ([#23](https://github.com/stevearc/quicker.nvim/issues/23)) ([cc8bb67](https://github.com/stevearc/quicker.nvim/commit/cc8bb67271c093a089d205def9dd69a188c45ae1)) +* toggle function for context ([#18](https://github.com/stevearc/quicker.nvim/issues/18)) ([049d665](https://github.com/stevearc/quicker.nvim/commit/049d66534d3de5920663ee1b8dd0096d70f55a67)) + + +### Bug Fixes + +* filter vim.NIL when deserializing buffer variables ([#30](https://github.com/stevearc/quicker.nvim/issues/30)) ([a3cf525](https://github.com/stevearc/quicker.nvim/commit/a3cf5256998f9387ad8e293c6f295d286be6453f)) + +## [1.2.0](https://github.com/stevearc/quicker.nvim/compare/v1.1.1...v1.2.0) (2024-11-06) + + +### Features + +* add command modifiers to the `toggle()` and `open()` APIs ([#24](https://github.com/stevearc/quicker.nvim/issues/24)) ([95a839f](https://github.com/stevearc/quicker.nvim/commit/95a839fafff1c0a7fe970492f5159f41a90974bf)) + + +### Bug Fixes + +* crash in highlighter ([11f9eb0](https://github.com/stevearc/quicker.nvim/commit/11f9eb0c803bb9ced8c6043805de89c62bd04515)) +* guard against out of date buffer contents ([1fc29de](https://github.com/stevearc/quicker.nvim/commit/1fc29de2172235c076aa1ead6f1ee772398de732)) +* trim_leading_whitespace works with mixed tabs and spaces ([#26](https://github.com/stevearc/quicker.nvim/issues/26)) ([46e0ad6](https://github.com/stevearc/quicker.nvim/commit/46e0ad6c6a1d998a294e13cbb8b7c398e140983a)) + +## [1.1.1](https://github.com/stevearc/quicker.nvim/compare/v1.1.0...v1.1.1) (2024-08-20) + + +### Bug Fixes + +* refresh replaces all item text with buffer source ([f28fca3](https://github.com/stevearc/quicker.nvim/commit/f28fca3863f8d3679e86d8ff30d023a43fba15c8)) + +## [1.1.0](https://github.com/stevearc/quicker.nvim/compare/v1.0.0...v1.1.0) (2024-08-20) + + +### Features + +* better support for lazy loading ([29ab2a6](https://github.com/stevearc/quicker.nvim/commit/29ab2a6d4771ace240f25df028129bfc85e16ffd)) +* display errors as virtual text when expanding context ([#16](https://github.com/stevearc/quicker.nvim/issues/16)) ([6b79167](https://github.com/stevearc/quicker.nvim/commit/6b79167543f1b18e76319217a29bb4e177a5e1ae)) +* quicker.refresh preserves and display diagnostic messages ([#19](https://github.com/stevearc/quicker.nvim/issues/19)) ([349e0de](https://github.com/stevearc/quicker.nvim/commit/349e0def74ddbfc47f64ca52202e84bedf064048)) + + +### Bug Fixes + +* editor works when filename is truncated ([7a64d4e](https://github.com/stevearc/quicker.nvim/commit/7a64d4ea2b641cc8671443d0ff26de2924894c9f)) +* **editor:** load buffer if necessary before save_changes ([#14](https://github.com/stevearc/quicker.nvim/issues/14)) ([59a610a](https://github.com/stevearc/quicker.nvim/commit/59a610a2163a51a019bde769bf2e2eec1654e4d4)) +* error when quickfix buffer is hidden and items are added ([#8](https://github.com/stevearc/quicker.nvim/issues/8)) ([a8b885b](https://github.com/stevearc/quicker.nvim/commit/a8b885be246666922aca7f296195986a1cae3344)) +* guard against double-replacing a diagnostic line ([2dc0f80](https://github.com/stevearc/quicker.nvim/commit/2dc0f800770f8956c24a6d70fa61e7ec2e102d8a)) +* **highlight:** check if src_line exists before trying to highlight it ([#6](https://github.com/stevearc/quicker.nvim/issues/6)) ([b6a3d2f](https://github.com/stevearc/quicker.nvim/commit/b6a3d2f6aed7882e8bea772f82ba80b5535157a9)) +* include number of files in editor message ([#13](https://github.com/stevearc/quicker.nvim/issues/13)) ([7d2f6d3](https://github.com/stevearc/quicker.nvim/commit/7d2f6d33c7d680b0a18580cfa5feb17302f389d4)) +* missing highlight groups for headers ([5dafd80](https://github.com/stevearc/quicker.nvim/commit/5dafd80225ba462517c38e7b176bd3df52ccfb35)) +* prevent error when treesitter parser is missing ([#4](https://github.com/stevearc/quicker.nvim/issues/4)) ([5cc096a](https://github.com/stevearc/quicker.nvim/commit/5cc096aad4ba1c1e17b6d76cb87fd7155cf9a559)) +* show filename for invalid items ([#11](https://github.com/stevearc/quicker.nvim/issues/11)) ([514817d](https://github.com/stevearc/quicker.nvim/commit/514817dfb0a2828fe2c6183f996a31847c8aa789)) + +## 1.0.0 (2024-08-07) + + +### Bug Fixes + +* guard against race condition in syntax highlighting ([#1](https://github.com/stevearc/quicker.nvim/issues/1)) ([03d9811](https://github.com/stevearc/quicker.nvim/commit/03d9811c8ac037e4e9c8f4ba0dfd1dff0367e0ac)) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/LICENSE b/mut/neovim/pack/plugins/start/quicker.nvim/LICENSE new file mode 100644 index 0000000..ce6136c --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Steven Arcangeli + +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/mut/neovim/pack/plugins/start/quicker.nvim/Makefile b/mut/neovim/pack/plugins/start/quicker.nvim/Makefile new file mode 100644 index 0000000..8643a8d --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/Makefile @@ -0,0 +1,52 @@ +## help: print this help message +.PHONY: help +help: + @echo 'Usage:' + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +## all: generate docs, lint, and run tests +.PHONY: all +all: doc lint test + +venv: + python3 -m venv venv + venv/bin/pip install -r scripts/requirements.txt + +## doc: generate documentation +.PHONY: doc +doc: scripts/nvim_doc_tools venv + venv/bin/python scripts/main.py generate + venv/bin/python scripts/main.py lint + +## test: run tests +.PHONY: test +test: + ./run_tests.sh + +## update_snapshots: Update the test snapshot files +.PHONY: update_snapshots +update_snapshots: + ./run_tests.sh --update + +## lint: run linters and LuaLS typechecking +.PHONY: lint +lint: scripts/nvim-typecheck-action fastlint + ./scripts/nvim-typecheck-action/typecheck.sh --workdir scripts/nvim-typecheck-action lua + +## fastlint: run only fast linters +.PHONY: fastlint +fastlint: scripts/nvim_doc_tools venv + venv/bin/python scripts/main.py lint + luacheck lua tests --formatter plain + stylua --check lua tests + +scripts/nvim_doc_tools: + git clone https://github.com/stevearc/nvim_doc_tools scripts/nvim_doc_tools + +scripts/nvim-typecheck-action: + git clone https://github.com/stevearc/nvim-typecheck-action scripts/nvim-typecheck-action + +## clean: reset the repository to a clean state +.PHONY: clean +clean: + rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action venv .testenv diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/README.md b/mut/neovim/pack/plugins/start/quicker.nvim/README.md new file mode 100644 index 0000000..6e07620 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/README.md @@ -0,0 +1,375 @@ +# quicker.nvim + +Improved UI and workflow for the Neovim quickfix + +<!-- TOC --> + +- [Requirements](#requirements) +- [Features](#features) +- [Installation](#installation) +- [Setup](#setup) +- [Options](#options) +- [Highlights](#highlights) +- [API](#api) +- [Other Plugins](#other-plugins) + +<!-- /TOC --> + +## Requirements + +- Neovim 0.10+ + +## Features + +- **Improved styling** - including syntax highlighting of grep results. +- **Show context lines** - easily view lines above and below the quickfix results. +- **Editable buffer** - make changes across your whole project by editing the quickfix buffer and `:w`. +- **API helpers** - some helper methods for common tasks, such as toggling the quickfix. + +**Improved styling** (colorscheme: [Duskfox](https://github.com/EdenEast/nightfox.nvim/)) \ +Before \ +<img width="695" alt="Screenshot 2024-07-30 at 6 03 39 PM" src="https://github.com/user-attachments/assets/8faa4790-8a7a-4d05-882e-c4e8e7653b00"> + +After \ +<img width="686" alt="Screenshot 2024-07-30 at 2 05 49 PM" src="https://github.com/user-attachments/assets/90cf87dd-83ec-4967-88aa-5ffe3e1e6623"> + +**Context lines** around the results \ +<img width="816" alt="Screenshot 2024-07-30 at 2 06 17 PM" src="https://github.com/user-attachments/assets/844445c9-328f-4f18-91d9-b32d32d3ef39"> + +**Editing the quickfix** to apply changes across multiple files + +https://github.com/user-attachments/assets/5065ac4d-ec24-49d1-a95d-232344b17484 + +## Installation + +quicker.nvim supports all the usual plugin managers + +<details> + <summary>lazy.nvim</summary> + +```lua +{ + 'stevearc/quicker.nvim', + event = "FileType qf", + ---@module "quicker" + ---@type quicker.SetupOptions + opts = {}, +} +``` + +</details> + +<details> + <summary>Packer</summary> + +```lua +require("packer").startup(function() + use({ + "stevearc/quicker.nvim", + config = function() + require("quicker").setup() + end, + }) +end) +``` + +</details> + +<details> + <summary>Paq</summary> + +```lua +require("paq")({ + { "stevearc/quicker.nvim" }, +}) +``` + +</details> + +<details> + <summary>vim-plug</summary> + +```vim +Plug 'stevearc/quicker.nvim' +``` + +</details> + +<details> + <summary>dein</summary> + +```vim +call dein#add('stevearc/quicker.nvim') +``` + +</details> + +<details> + <summary>Pathogen</summary> + +```sh +git clone --depth=1 https://github.com/stevearc/quicker.nvim.git ~/.vim/bundle/ +``` + +</details> + +<details> + <summary>Neovim native package</summary> + +```sh +git clone --depth=1 https://github.com/stevearc/quicker.nvim.git \ + "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/pack/quicker/start/quicker.nvim +``` + +</details> + +## Setup + +You will need to call `setup()` for quicker to start working + +```lua +require("quicker").setup() +``` + +It's not required to pass in any options, but you may wish to to set some keymaps. + +```lua +vim.keymap.set("n", "<leader>q", function() + require("quicker").toggle() +end, { + desc = "Toggle quickfix", +}) +vim.keymap.set("n", "<leader>l", function() + require("quicker").toggle({ loclist = true }) +end, { + desc = "Toggle loclist", +}) +require("quicker").setup({ + keys = { + { + ">", + function() + require("quicker").expand({ before = 2, after = 2, add_to_existing = true }) + end, + desc = "Expand quickfix context", + }, + { + "<", + function() + require("quicker").collapse() + end, + desc = "Collapse quickfix context", + }, + }, +}) +``` + +## Options + +A complete list of all configuration options + +<!-- OPTIONS --> +```lua +require("quicker").setup({ + -- Local options to set for quickfix + opts = { + buflisted = false, + number = false, + relativenumber = false, + signcolumn = "auto", + winfixheight = true, + wrap = false, + }, + -- Set to false to disable the default options in `opts` + use_default_opts = true, + -- Keymaps to set for the quickfix buffer + keys = { + -- { ">", "<cmd>lua require('quicker').expand()<CR>", desc = "Expand quickfix content" }, + }, + -- Callback function to run any custom logic or keymaps for the quickfix buffer + on_qf = function(bufnr) end, + edit = { + -- Enable editing the quickfix like a normal buffer + enabled = true, + -- Set to true to write buffers after applying edits. + -- Set to "unmodified" to only write unmodified buffers. + autosave = "unmodified", + }, + -- Keep the cursor to the right of the filename and lnum columns + constrain_cursor = true, + highlight = { + -- Use treesitter highlighting + treesitter = true, + -- Use LSP semantic token highlighting + lsp = true, + -- Load the referenced buffers to apply more accurate highlights (may be slow) + load_buffers = true, + }, + follow = { + -- When quickfix window is open, scroll to closest item to the cursor + enabled = false, + }, + -- Map of quickfix item type to icon + type_icons = { + E = " ", + W = " ", + I = " ", + N = " ", + H = " ", + }, + -- Border characters + borders = { + vert = "┃", + -- Strong headers separate results from different files + strong_header = "━", + strong_cross = "╋", + strong_end = "┫", + -- Soft headers separate results within the same file + soft_header = "╌", + soft_cross = "╂", + soft_end = "┨", + }, + -- How to trim the leading whitespace from results. Can be 'all', 'common', or false + trim_leading_whitespace = "common", + -- Maximum width of the filename column + max_filename_width = function() + return math.floor(math.min(95, vim.o.columns / 2)) + end, + -- How far the header should extend to the right + header_length = function(type, start_col) + return vim.o.columns - start_col + end, +}) +``` + +<!-- /OPTIONS --> + +## Highlights + +These are the highlight groups that are used to style the quickfix buffer. You can set these +highlight groups yourself or use `:help winhighlight` in the setup `opts` option to override them +for just the quickfix window. + +- `QuickFixHeaderHard` - Used for the header that divides results from different files +- `QuickFixHeaderSoft` - Used for the header that divides results within the same file +- `Delimiter` - Used for the divider between filename, line number, and text +- `QuickFixLineNr` - Used for the line number +- `QuickFixFilename` - Used for the filename +- `QuickFixFilenameInvalid` - Used for the filename when `valid = 0` +- `DiagnosticSign*` - Used for the signs that display the quickfix error type + +## API + +<!-- API --> + +### expand(opts) + +`expand(opts)` \ +Expand the context around the quickfix results. + +| Param | Type | Desc | +| ---------------- | ------------------------- | -------------------------------------------------------------- | +| opts | `nil\|quicker.ExpandOpts` | | +| >before | `nil\|integer` | Number of lines of context to show before the line (default 2) | +| >after | `nil\|integer` | Number of lines of context to show after the line (default 2) | +| >add_to_existing | `nil\|boolean` | | +| >loclist_win | `nil\|integer` | | + +**Note:** +<pre> +If there are multiple quickfix items for the same line of a file, only the first +one will remain after calling expand(). +</pre> + +### collapse() + +`collapse()` \ +Collapse the context around quickfix results, leaving only the `valid` items. + + +### toggle_expand(opts) + +`toggle_expand(opts)` \ +Toggle the expanded context around the quickfix results. + +| Param | Type | Desc | +| ---------------- | ------------------------- | -------------------------------------------------------------- | +| opts | `nil\|quicker.ExpandOpts` | | +| >before | `nil\|integer` | Number of lines of context to show before the line (default 2) | +| >after | `nil\|integer` | Number of lines of context to show after the line (default 2) | +| >add_to_existing | `nil\|boolean` | | +| >loclist_win | `nil\|integer` | | + +### refresh(loclist_win, opts) + +`refresh(loclist_win, opts)` \ +Update the quickfix list with the current buffer text for each item. + +| Param | Type | Desc | +| ----------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| loclist_win | `nil\|integer` | | +| opts | `nil\|quicker.RefreshOpts` | | +| >keep_diagnostics | `nil\|boolean` | If a line has a diagnostic type, keep the original text and display it as virtual text after refreshing from source. | + +### is_open(loclist_win) + +`is_open(loclist_win)` + +| Param | Type | Desc | +| ----------- | -------------- | ---------------------------------------------------------------------- | +| loclist_win | `nil\|integer` | Check if loclist is open for the given window. If nil, check quickfix. | + +### toggle(opts) + +`toggle(opts)` \ +Toggle the quickfix or loclist window. + +| Param | Type | Desc | +| -------------- | -------------------------- | ----------------------------------------------------------------------------------- | +| opts | `nil\|quicker.OpenOpts` | | +| >loclist | `nil\|boolean` | Toggle the loclist instead of the quickfix list | +| >focus | `nil\|boolean` | Focus the quickfix window after toggling (default false) | +| >height | `nil\|integer` | Height of the quickfix window when opened. Defaults to number of items in the list. | +| >min_height | `nil\|integer` | Minimum height of the quickfix window. Default 4. | +| >max_height | `nil\|integer` | Maximum height of the quickfix window. Default 10. | +| >open_cmd_mods | `nil\|quicker.OpenCmdMods` | A table of modifiers for the quickfix or loclist open commands. | + +### open(opts) + +`open(opts)` \ +Open the quickfix or loclist window. + +| Param | Type | Desc | +| -------------- | -------------------------- | ----------------------------------------------------------------------------------- | +| opts | `nil\|quicker.OpenOpts` | | +| >loclist | `nil\|boolean` | Toggle the loclist instead of the quickfix list | +| >focus | `nil\|boolean` | Focus the quickfix window after toggling (default false) | +| >height | `nil\|integer` | Height of the quickfix window when opened. Defaults to number of items in the list. | +| >min_height | `nil\|integer` | Minimum height of the quickfix window. Default 4. | +| >max_height | `nil\|integer` | Maximum height of the quickfix window. Default 10. | +| >open_cmd_mods | `nil\|quicker.OpenCmdMods` | A table of modifiers for the quickfix or loclist open commands. | + +### close(opts) + +`close(opts)` \ +Close the quickfix or loclist window. + +| Param | Type | Desc | +| -------- | ------------------------ | ---------------------------------------------- | +| opts | `nil\|quicker.CloseOpts` | | +| >loclist | `nil\|boolean` | Close the loclist instead of the quickfix list | +<!-- /API --> + +## Other Plugins + +In general quicker.nvim should play nice with other quickfix plugins (🟢), except if they change the +format of the quickfix buffer. Quicker.nvim relies on owning the `:help quickfixtextfunc` for the +other features to function, so some other plugins you may need to disable or not use parts of their +functionality (🟡). Some plugins have features that completely conflict with quicker.nvim (🔴). + +- 🟢 [nvim-bqf](https://github.com/kevinhwang91/nvim-bqf) - Another bundle of several improvements including a floating preview window and fzf integration. +- 🟢 [vim-qf](https://github.com/romainl/vim-qf) - Adds some useful mappings and default behaviors. +- 🟡 [trouble.nvim](https://github.com/folke/trouble.nvim) - A custom UI for displaying quickfix and many other lists. Does not conflict with quicker.nvim, but instead presents an alternative way to manage and view the quickfix. +- 🟡 [listish.nvim](https://github.com/arsham/listish.nvim) - Provides utilities for adding items to the quickfix and theming (which conflicts with quicker.nvim). +- 🔴 [quickfix-reflector.vim](https://github.com/stefandtw/quickfix-reflector.vim) - Also provides an "editable quickfix". I used this for many years and would recommend it. +- 🔴 [replacer.nvim](https://github.com/gabrielpoca/replacer.nvim) - Another "editable quickfix" plugin. diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/doc/quicker.txt b/mut/neovim/pack/plugins/start/quicker.nvim/doc/quicker.txt new file mode 100644 index 0000000..cf3dbe7 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/doc/quicker.txt @@ -0,0 +1,181 @@ +*quicker.txt* +*Quicker* *quicker* *quicker.nvim* +-------------------------------------------------------------------------------- +CONTENTS *quicker-contents* + + 1. Options |quicker-options| + 2. Api |quicker-api| + +-------------------------------------------------------------------------------- +OPTIONS *quicker-options* + +>lua + require("quicker").setup({ + -- Local options to set for quickfix + opts = { + buflisted = false, + number = false, + relativenumber = false, + signcolumn = "auto", + winfixheight = true, + wrap = false, + }, + -- Set to false to disable the default options in `opts` + use_default_opts = true, + -- Keymaps to set for the quickfix buffer + keys = { + -- { ">", "<cmd>lua require('quicker').expand()<CR>", desc = "Expand quickfix content" }, + }, + -- Callback function to run any custom logic or keymaps for the quickfix buffer + on_qf = function(bufnr) end, + edit = { + -- Enable editing the quickfix like a normal buffer + enabled = true, + -- Set to true to write buffers after applying edits. + -- Set to "unmodified" to only write unmodified buffers. + autosave = "unmodified", + }, + -- Keep the cursor to the right of the filename and lnum columns + constrain_cursor = true, + highlight = { + -- Use treesitter highlighting + treesitter = true, + -- Use LSP semantic token highlighting + lsp = true, + -- Load the referenced buffers to apply more accurate highlights (may be slow) + load_buffers = true, + }, + follow = { + -- When quickfix window is open, scroll to closest item to the cursor + enabled = false, + }, + -- Map of quickfix item type to icon + type_icons = { + E = " ", + W = " ", + I = " ", + N = " ", + H = " ", + }, + -- Border characters + borders = { + vert = "┃", + -- Strong headers separate results from different files + strong_header = "━", + strong_cross = "╋", + strong_end = "┫", + -- Soft headers separate results within the same file + soft_header = "╌", + soft_cross = "╂", + soft_end = "┨", + }, + -- How to trim the leading whitespace from results. Can be 'all', 'common', or false + trim_leading_whitespace = "common", + -- Maximum width of the filename column + max_filename_width = function() + return math.floor(math.min(95, vim.o.columns / 2)) + end, + -- How far the header should extend to the right + header_length = function(type, start_col) + return vim.o.columns - start_col + end, + }) +< + +-------------------------------------------------------------------------------- +API *quicker-api* + +expand({opts}) *quicker.expand* + Expand the context around the quickfix results. + + Parameters: + {opts} `nil|quicker.ExpandOpts` + {before} `nil|integer` Number of lines of context to show + before the line (default 2) + {after} `nil|integer` Number of lines of context to show + after the line (default 2) + {add_to_existing} `nil|boolean` + {loclist_win} `nil|integer` + + Note: + If there are multiple quickfix items for the same line of a file, only the first + one will remain after calling expand(). + +collapse() *quicker.collapse* + Collapse the context around quickfix results, leaving only the `valid` + items. + + +toggle_expand({opts}) *quicker.toggle_expand* + Toggle the expanded context around the quickfix results. + + Parameters: + {opts} `nil|quicker.ExpandOpts` + {before} `nil|integer` Number of lines of context to show + before the line (default 2) + {after} `nil|integer` Number of lines of context to show + after the line (default 2) + {add_to_existing} `nil|boolean` + {loclist_win} `nil|integer` + +refresh({loclist_win}, {opts}) *quicker.refresh* + Update the quickfix list with the current buffer text for each item. + + Parameters: + {loclist_win} `nil|integer` + {opts} `nil|quicker.RefreshOpts` + {keep_diagnostics} `nil|boolean` If a line has a diagnostic type, keep + the original text and display it as virtual text + after refreshing from source. + +is_open({loclist_win}) *quicker.is_open* + + Parameters: + {loclist_win} `nil|integer` Check if loclist is open for the given window. + If nil, check quickfix. + +toggle({opts}) *quicker.toggle* + Toggle the quickfix or loclist window. + + Parameters: + {opts} `nil|quicker.OpenOpts` + {loclist} `nil|boolean` Toggle the loclist instead of the + quickfix list + {focus} `nil|boolean` Focus the quickfix window after toggling + (default false) + {height} `nil|integer` Height of the quickfix window when + opened. Defaults to number of items in the list. + {min_height} `nil|integer` Minimum height of the quickfix window. + Default 4. + {max_height} `nil|integer` Maximum height of the quickfix window. + Default 10. + {open_cmd_mods} `nil|quicker.OpenCmdMods` A table of modifiers for the + quickfix or loclist open commands. + +open({opts}) *quicker.open* + Open the quickfix or loclist window. + + Parameters: + {opts} `nil|quicker.OpenOpts` + {loclist} `nil|boolean` Toggle the loclist instead of the + quickfix list + {focus} `nil|boolean` Focus the quickfix window after toggling + (default false) + {height} `nil|integer` Height of the quickfix window when + opened. Defaults to number of items in the list. + {min_height} `nil|integer` Minimum height of the quickfix window. + Default 4. + {max_height} `nil|integer` Maximum height of the quickfix window. + Default 10. + {open_cmd_mods} `nil|quicker.OpenCmdMods` A table of modifiers for the + quickfix or loclist open commands. + +close({opts}) *quicker.close* + Close the quickfix or loclist window. + + Parameters: + {opts} `nil|quicker.CloseOpts` + {loclist} `nil|boolean` Close the loclist instead of the quickfix list + +================================================================================ +vim:tw=80:ts=2:ft=help:norl:syntax=help: diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/config.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/config.lua new file mode 100644 index 0000000..716f010 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/config.lua @@ -0,0 +1,189 @@ +local default_config = { + -- Local options to set for quickfix + opts = { + buflisted = false, + number = false, + relativenumber = false, + signcolumn = "auto", + winfixheight = true, + wrap = false, + }, + -- Set to false to disable the default options in `opts` + use_default_opts = true, + -- Keymaps to set for the quickfix buffer + keys = { + -- { ">", "<cmd>lua require('quicker').expand()<CR>", desc = "Expand quickfix content" }, + }, + -- Callback function to run any custom logic or keymaps for the quickfix buffer + on_qf = function(bufnr) end, + edit = { + -- Enable editing the quickfix like a normal buffer + enabled = true, + -- Set to true to write buffers after applying edits. + -- Set to "unmodified" to only write unmodified buffers. + autosave = "unmodified", + }, + -- Keep the cursor to the right of the filename and lnum columns + constrain_cursor = true, + highlight = { + -- Use treesitter highlighting + treesitter = true, + -- Use LSP semantic token highlighting + lsp = true, + -- Load the referenced buffers to apply more accurate highlights (may be slow) + load_buffers = true, + }, + follow = { + -- When quickfix window is open, scroll to closest item to the cursor + enabled = false, + }, + -- Map of quickfix item type to icon + type_icons = { + E = " ", + W = " ", + I = " ", + N = " ", + H = " ", + }, + -- Border characters + borders = { + vert = "┃", + -- Strong headers separate results from different files + strong_header = "━", + strong_cross = "╋", + strong_end = "┫", + -- Soft headers separate results within the same file + soft_header = "╌", + soft_cross = "╂", + soft_end = "┨", + }, + -- How to trim the leading whitespace from results. Can be 'all', 'common', or false + trim_leading_whitespace = "common", + -- Maximum width of the filename column + max_filename_width = function() + return math.floor(math.min(95, vim.o.columns / 2)) + end, + -- How far the header should extend to the right + header_length = function(type, start_col) + return vim.o.columns - start_col + end, +} + +---@alias quicker.TrimEnum "all"|"common"|false + +---@class quicker.Config +---@field on_qf fun(bufnr: number) +---@field opts table<string, any> +---@field keys quicker.Keymap[] +---@field use_default_opts boolean +---@field constrain_cursor boolean +---@field highlight quicker.HighlightConfig +---@field follow quicker.FollowConfig +---@field edit quicker.EditConfig +---@field type_icons table<string, string> +---@field borders quicker.Borders +---@field trim_leading_whitespace quicker.TrimEnum +---@field max_filename_width fun(): integer +---@field header_length fun(type: "hard"|"soft", start_col: integer): integer +local M = {} + +---@class (exact) quicker.SetupOptions +---@field on_qf? fun(bufnr: number) Callback function to run any custom logic or keymaps for the quickfix buffer +---@field opts? table<string, any> Local options to set for quickfix +---@field keys? quicker.Keymap[] Keymaps to set for the quickfix buffer +---@field use_default_opts? boolean Set to false to disable the default options in `opts` +---@field constrain_cursor? boolean Keep the cursor to the right of the filename and lnum columns +---@field highlight? quicker.SetupHighlightConfig Configure syntax highlighting +---@field follow? quicker.SetupFollowConfig Configure cursor following +---@field edit? quicker.SetupEditConfig +---@field type_icons? table<string, string> Map of quickfix item type to icon +---@field borders? quicker.SetupBorders Characters used for drawing the borders +---@field trim_leading_whitespace? quicker.TrimEnum How to trim the leading whitespace from results +---@field max_filename_width? fun(): integer Maximum width of the filename column +---@field header_length? fun(type: "hard"|"soft", start_col: integer): integer How far the header should extend to the right + +local has_setup = false +---@param opts? quicker.SetupOptions +M.setup = function(opts) + opts = opts or {} + local new_conf = vim.tbl_deep_extend("keep", opts, default_config) + + for k, v in pairs(new_conf) do + M[k] = v + end + + -- Shim for when this was only a boolean. 'true' meant 'common' + if M.trim_leading_whitespace == true then + M.trim_leading_whitespace = "common" + end + + -- Remove the default opts values if use_default_opts is false + if not new_conf.use_default_opts then + M.opts = opts.opts or {} + end + has_setup = true +end + +---@class (exact) quicker.Keymap +---@field [1] string Key sequence +---@field [2] any Command to run +---@field desc? string +---@field mode? string +---@field expr? boolean +---@field nowait? boolean +---@field remap? boolean +---@field replace_keycodes? boolean +---@field silent? boolean + +---@class (exact) quicker.Borders +---@field vert string +---@field strong_header string +---@field strong_cross string +---@field strong_end string +---@field soft_header string +---@field soft_cross string +---@field soft_end string + +---@class (exact) quicker.SetupBorders +---@field vert? string +---@field strong_header? string Strong headers separate results from different files +---@field strong_cross? string +---@field strong_end? string +---@field soft_header? string Soft headers separate results within the same file +---@field soft_cross? string +---@field soft_end? string + +---@class (exact) quicker.HighlightConfig +---@field treesitter boolean +---@field lsp boolean +---@field load_buffers boolean + +---@class (exact) quicker.SetupHighlightConfig +---@field treesitter? boolean Enable treesitter syntax highlighting +---@field lsp? boolean Use LSP semantic token highlighting +---@field load_buffers? boolean Load the referenced buffers to apply more accurate highlights (may be slow) + +---@class (exact) quicker.FollowConfig +---@field enabled boolean + +---@class (exact) quicker.SetupFollowConfig +---@field enabled? boolean + +---@class (exact) quicker.EditConfig +---@field enabled boolean +---@field autosave boolean|"unmodified" + +---@class (exact) quicker.SetupEditConfig +---@field enabled? boolean +---@field autosave? boolean|"unmodified" + +return setmetatable(M, { + -- If the user hasn't called setup() yet, make sure we correctly set up the config object so there + -- aren't random crashes. + __index = function(self, key) + if not has_setup then + M.setup() + end + return rawget(self, key) + end, +}) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/context.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/context.lua new file mode 100644 index 0000000..f5bbb87 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/context.lua @@ -0,0 +1,315 @@ +local util = require("quicker.util") + +local M = {} + +---@class (exact) quicker.QFContext +---@field num_before integer +---@field num_after integer + +---@class (exact) quicker.ExpandOpts +---@field before? integer Number of lines of context to show before the line (default 2) +---@field after? integer Number of lines of context to show after the line (default 2) +---@field add_to_existing? boolean +---@field loclist_win? integer + +---@param item QuickFixItem +---@param new_text string +local function update_item_text_keep_diagnostics(item, new_text) + -- If this is an "error" item, replace the text with the source line and store that text + -- in the user data so we can add it as virtual text later + if item.type ~= "" and not vim.endswith(new_text, item.text) then + local user_data = util.get_user_data(item) + if not user_data.error_text then + user_data.error_text = item.text + item.user_data = user_data + end + end + item.text = new_text +end + +---@param opts? quicker.ExpandOpts +function M.expand(opts) + opts = opts or {} + if not opts.loclist_win and util.get_win_type(0) == "l" then + opts.loclist_win = vim.api.nvim_get_current_win() + end + local qf_list + if opts.loclist_win then + qf_list = vim.fn.getloclist(opts.loclist_win, { all = 0 }) + else + qf_list = vim.fn.getqflist({ all = 0 }) + end + local winid = qf_list.winid + if not winid then + vim.notify("Cannot find quickfix window", vim.log.levels.ERROR) + return + end + local ctx = qf_list.context or {} + if type(ctx) ~= "table" then + -- If the quickfix had a non-table context, we're going to have to overwrite it + ctx = {} + end + ---@type quicker.QFContext + local quicker_ctx = ctx.quicker + if not quicker_ctx then + quicker_ctx = { num_before = 0, num_after = 0 } + ctx.quicker = quicker_ctx + end + local curpos = vim.api.nvim_win_get_cursor(winid)[1] + local cur_item = qf_list.items[curpos] + local newpos + + -- calculate the number of lines to show before and after the current line + local num_before = opts.before or 2 + if opts.add_to_existing then + num_before = num_before + quicker_ctx.num_before + end + num_before = math.max(0, num_before) + quicker_ctx.num_before = num_before + local num_after = opts.after or 2 + if opts.add_to_existing then + num_after = num_after + quicker_ctx.num_after + end + num_after = math.max(0, num_after) + quicker_ctx.num_after = num_after + + local items = {} + ---@type nil|QuickFixItem + local prev_item + ---@param i integer + ---@return nil|QuickFixItem + local function get_next_item(i) + local item = qf_list.items[i] + for j = i + 1, #qf_list.items do + local next_item = qf_list.items[j] + -- Next valid item that is on a different line (since we dedupe same-line items) + if + next_item.valid == 1 and (item.bufnr ~= next_item.bufnr or item.lnum ~= next_item.lnum) + then + return next_item + end + end + end + + for i, item in ipairs(qf_list.items) do + (function() + ---@cast item QuickFixItem + if item.valid == 0 or item.bufnr == 0 then + return + end + + if not vim.api.nvim_buf_is_loaded(item.bufnr) then + vim.fn.bufload(item.bufnr) + end + + local overlaps_previous = false + local header_type = "hard" + local low = math.max(0, item.lnum - 1 - num_before) + if prev_item then + if prev_item.bufnr == item.bufnr then + -- If this is the second match on the same line, skip this item + if prev_item.lnum == item.lnum then + return + end + header_type = "soft" + if prev_item.lnum + num_after >= low then + low = math.min(item.lnum - 1, prev_item.lnum + num_after) + overlaps_previous = true + end + end + end + + local high = item.lnum + num_after + local next_item = get_next_item(i) + if next_item then + if next_item.bufnr == item.bufnr and next_item.lnum <= high then + high = next_item.lnum - 1 + end + end + + local item_start_idx = #items + local lines = vim.api.nvim_buf_get_lines(item.bufnr, low, high, false) + for j, line in ipairs(lines) do + if j + low == item.lnum then + update_item_text_keep_diagnostics(item, line) + table.insert(items, item) + else + table.insert(items, { + bufnr = item.bufnr, + lnum = low + j, + text = line, + valid = 0, + user_data = { lnum = low + j }, + }) + end + if cur_item.bufnr == item.bufnr and cur_item.lnum == low + j then + newpos = #items + end + end + + -- Add the header to the first item in this sequence, if one is needed + if prev_item and not overlaps_previous then + local first_item = items[item_start_idx + 1] + if first_item then + first_item.user_data = first_item.user_data or {} + first_item.user_data.header = header_type + end + end + + prev_item = item + end)() + + if i == curpos and not newpos then + newpos = #items + end + end + + if opts.loclist_win then + vim.fn.setloclist( + opts.loclist_win, + {}, + "r", + { items = items, title = qf_list.title, context = ctx } + ) + else + vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = ctx }) + end + + pcall(vim.api.nvim_win_set_cursor, qf_list.winid, { newpos, 0 }) +end + +---@class (exact) quicker.CollapseArgs +---@field loclist_win? integer +--- +function M.collapse(opts) + opts = opts or {} + if not opts.loclist_win and util.get_win_type(0) == "l" then + opts.loclist_win = vim.api.nvim_get_current_win() + end + local curpos = vim.api.nvim_win_get_cursor(0)[1] + local qf_list + if opts.loclist_win then + qf_list = vim.fn.getloclist(opts.loclist_win, { all = 0 }) + else + qf_list = vim.fn.getqflist({ all = 0 }) + end + local items = {} + local last_item + for i, item in ipairs(qf_list.items) do + if item.valid == 1 then + if item.user_data then + -- Clear the header, if present + item.user_data.header = nil + end + table.insert(items, item) + if i <= curpos then + last_item = #items + end + end + end + + vim.tbl_filter(function(item) + return item.valid == 1 + end, qf_list.items) + + local ctx = qf_list.context or {} + if type(ctx) == "table" then + local quicker_ctx = ctx.quicker + if quicker_ctx then + quicker_ctx = { num_before = 0, num_after = 0 } + ctx.quicker = quicker_ctx + end + end + + if opts.loclist_win then + vim.fn.setloclist( + opts.loclist_win, + {}, + "r", + { items = items, title = qf_list.title, context = qf_list.context } + ) + else + vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = qf_list.context }) + end + if qf_list.winid then + if last_item then + vim.api.nvim_win_set_cursor(qf_list.winid, { last_item, 0 }) + end + end +end + +---@param opts? quicker.ExpandOpts +function M.toggle(opts) + opts = opts or {} + local ctx + if opts.loclist_win then + ctx = vim.fn.getloclist(opts.loclist_win, { context = 0 }).context + else + ctx = vim.fn.getqflist({ context = 0 }).context + end + + if + type(ctx) == "table" + and ctx.quicker + and (ctx.quicker.num_before > 0 or ctx.quicker.num_after > 0) + then + M.collapse() + else + M.expand(opts) + end +end + +---@class (exact) quicker.RefreshOpts +---@field keep_diagnostics? boolean If a line has a diagnostic type, keep the original text and display it as virtual text after refreshing from source. + +---@param loclist_win? integer +---@param opts? quicker.RefreshOpts +function M.refresh(loclist_win, opts) + opts = vim.tbl_extend("keep", opts or {}, { keep_diagnostics = true }) + if not loclist_win then + local ok, qf = pcall(vim.fn.getloclist, 0, { filewinid = 0 }) + if ok and qf.filewinid and qf.filewinid ~= 0 then + loclist_win = qf.filewinid + end + end + + local qf_list + if loclist_win then + qf_list = vim.fn.getloclist(loclist_win, { all = 0 }) + else + qf_list = vim.fn.getqflist({ all = 0 }) + end + + local items = {} + for _, item in ipairs(qf_list.items) do + if item.bufnr ~= 0 and item.lnum ~= 0 then + if not vim.api.nvim_buf_is_loaded(item.bufnr) then + vim.fn.bufload(item.bufnr) + end + local line = vim.api.nvim_buf_get_lines(item.bufnr, item.lnum - 1, item.lnum, false)[1] + if line then + if opts.keep_diagnostics then + update_item_text_keep_diagnostics(item, line) + else + item.text = line + end + table.insert(items, item) + end + else + table.insert(items, item) + end + end + + if loclist_win then + vim.fn.setloclist( + loclist_win, + {}, + "r", + { items = items, title = qf_list.title, context = qf_list.context } + ) + else + vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = qf_list.context }) + end +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/cursor.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/cursor.lua new file mode 100644 index 0000000..58ee587 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/cursor.lua @@ -0,0 +1,44 @@ +local M = {} + +local function constrain_cursor() + local display = require("quicker.display") + local cur = vim.api.nvim_win_get_cursor(0) + local line = vim.api.nvim_buf_get_lines(0, cur[1] - 1, cur[1], true)[1] + local idx = line:find(display.EM_QUAD, 1, true) + if not idx then + return + end + local min_col = idx + display.EM_QUAD_LEN - 1 + if cur[2] < min_col then + vim.api.nvim_win_set_cursor(0, { cur[1], min_col }) + end +end + +---@param bufnr number +function M.constrain_cursor(bufnr) + -- HACK: we have to defer this call because sometimes the autocmds don't take effect. + vim.schedule(function() + if not vim.api.nvim_buf_is_valid(bufnr) then + return + end + local aug = vim.api.nvim_create_augroup("quicker", { clear = false }) + vim.api.nvim_create_autocmd("InsertEnter", { + desc = "Constrain quickfix cursor position", + group = aug, + nested = true, + buffer = bufnr, + -- For some reason the cursor bounces back to its original position, + -- so we have to defer the call + callback = vim.schedule_wrap(constrain_cursor), + }) + vim.api.nvim_create_autocmd({ "CursorMoved", "ModeChanged" }, { + desc = "Constrain quickfix cursor position", + nested = true, + group = aug, + buffer = bufnr, + callback = constrain_cursor, + }) + end) +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/display.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/display.lua new file mode 100644 index 0000000..5c551d3 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/display.lua @@ -0,0 +1,601 @@ +local config = require("quicker.config") +local fs = require("quicker.fs") +local highlight = require("quicker.highlight") +local util = require("quicker.util") + +local M = {} + +local EM_QUAD = " " +local EM_QUAD_LEN = EM_QUAD:len() +M.EM_QUAD = EM_QUAD +M.EM_QUAD_LEN = EM_QUAD_LEN + +---@class (exact) QuickFixUserData +---@field header? "hard"|"soft" When present, this line is a header +---@field lnum? integer Encode the lnum separately for valid=0 items +---@field error_text? string Error text to be added as virtual text on the line + +---@class (exact) QuickFixItem +---@field text string +---@field type string +---@field lnum integer line number in the buffer (first line is 1) +---@field end_lnum integer end of line number if the item is multiline +---@field col integer column number (first column is 1) +---@field end_col integer end of column number if the item has range +---@field vcol 0|1 if true "col" is visual column. If false "col" is byte index +---@field nr integer error number +---@field pattern string search pattern used to locate the error +---@field bufnr integer number of buffer that has the file name +---@field module string +---@field valid 0|1 +---@field user_data? any + +---@param type string +---@return string +local function get_icon(type) + return config.type_icons[type:upper()] or "U" +end + +local sign_highlight_map = { + E = "DiagnosticSignError", + W = "DiagnosticSignWarn", + I = "DiagnosticSignInfo", + H = "DiagnosticSignHint", + N = "DiagnosticSignHint", +} +local virt_text_highlight_map = { + E = "DiagnosticVirtualTextError", + W = "DiagnosticVirtualTextWarn", + I = "DiagnosticVirtualTextInfo", + H = "DiagnosticVirtualTextHint", + N = "DiagnosticVirtualTextHint", +} + +---@param item QuickFixItem +M.get_filename_from_item = function(item) + if item.module and item.module ~= "" then + return item.module + elseif item.bufnr > 0 then + local bufname = vim.api.nvim_buf_get_name(item.bufnr) + local path = fs.shorten_path(bufname) + local max_len = config.max_filename_width() + if max_len == 0 then + return "" + elseif path:len() > max_len then + path = "…" .. path:sub(path:len() - max_len - 1) + end + return path + else + return "" + end +end + +local _col_width_cache = {} +---@param id integer +---@param items QuickFixItem[] +---@return integer +local function get_cached_qf_col_width(id, items) + local cached = _col_width_cache[id] + if not cached or cached[2] ~= #items then + local max_len = 0 + for _, item in ipairs(items) do + max_len = math.max(max_len, vim.api.nvim_strwidth(M.get_filename_from_item(item))) + end + + cached = { max_len, #items } + _col_width_cache[id] = cached + end + return cached[1] +end + +---@param items QuickFixItem[] +---@return table<integer, string> +local function calc_whitespace_prefix(items) + local prefixes = {} + if config.trim_leading_whitespace ~= "common" then + return prefixes + end + + for _, item in ipairs(items) do + if item.bufnr ~= 0 and not item.text:match("^%s*$") then + local prefix = prefixes[item.bufnr] + if not prefix or not vim.startswith(item.text, prefix) then + local new_prefix = item.text:match("^%s*") + + -- The new line should have strictly less whitespace as the previous line. If not, then + -- there is some whitespace disagreement (e.g. tabs vs spaces) and we should not try to trim + -- anything. + if prefix and not vim.startswith(prefix, new_prefix) then + new_prefix = "" + end + prefixes[item.bufnr] = new_prefix + + if new_prefix == "" then + break + end + end + end + end + return prefixes +end + +-- Highlighting can be slow because it requires loading buffers and parsing them with treesitter, so +-- we pipeline it and break it up with defers to keep the editor responsive. +local add_qf_highlights +-- We have two queues, one to apply "fast" highlights, and one that will load the buffer (slow) +-- and then apply more correct highlights. The second queue is always processed after the first. +local _pending_fast_highlights = {} +local _pending_bufload_highlights = {} +local _running = false +local function do_next_highlight() + if _running then + return + end + _running = true + + local next_info = table.remove(_pending_fast_highlights, 1) + if not next_info then + next_info = table.remove(_pending_bufload_highlights, 1) + end + + if next_info then + local ok, err = xpcall(add_qf_highlights, debug.traceback, next_info) + if not ok then + vim.api.nvim_err_writeln(err) + end + else + _running = false + return + end + + vim.defer_fn(function() + _running = false + do_next_highlight() + end, 20) +end + +---@param queue QuickFixTextFuncInfo[] +---@param info QuickFixTextFuncInfo +local function add_info_to_queue(queue, info) + for _, i in ipairs(queue) do + -- If we're already processing a highlight for this quickfix, just expand the range + if i.id == info.id and i.winid == info.winid and i.quickfix == info.quickfix then + i.start_idx = math.min(i.start_idx, info.start_idx) + i.end_idx = math.max(i.end_idx, info.end_idx) + return + end + end + table.insert(queue, info) +end + +---@param info QuickFixTextFuncInfo +local function schedule_highlights(info) + -- If this info already has force_bufload, then we don't want to add it to the first queue. + if not info.force_bufload then + add_info_to_queue(_pending_fast_highlights, info) + end + + if config.highlight.load_buffers then + local info2 = vim.deepcopy(info) + info2.force_bufload = true + add_info_to_queue(_pending_bufload_highlights, info2) + end + + vim.schedule(do_next_highlight) +end + +---@param qfbufnr integer +---@param item QuickFixItem +---@param line string +---@param lnum integer +local function add_item_highlights_from_buf(qfbufnr, item, line, lnum) + local prefixes = vim.b[qfbufnr].qf_prefixes or {} + local ns = vim.api.nvim_create_namespace("quicker_highlights") + -- TODO re-apply highlights when a buffer is loaded or a LSP receives semantic tokens + local src_line = vim.api.nvim_buf_get_lines(item.bufnr, item.lnum - 1, item.lnum, false)[1] + if not src_line then + return + end + + -- If the lines differ only in leading whitespace, we should add highlights anyway and adjust + -- the offset. + local item_space = item.text:match("^%s*"):len() + local src_space = src_line:match("^%s*"):len() + + -- Only add highlights if the text in the quickfix matches the source line + if item.text:sub(item_space + 1) == src_line:sub(src_space + 1) then + local offset = line:find(EM_QUAD, 1, true) + EM_QUAD_LEN - 1 + local prefix = prefixes[item.bufnr] + if type(prefix) == "string" then + -- Since prefixes get deserialized from vim.b, if there are holes in the map they get + -- filled with `vim.NIL`, so we have to check that the retrieved value is a string. + offset = offset - prefix:len() + end + offset = offset - src_space + item_space + if config.trim_leading_whitespace == "all" then + offset = offset - item_space + end + + -- Add treesitter highlights + if config.highlight.treesitter then + for _, hl in ipairs(highlight.buf_get_ts_highlights(item.bufnr, item.lnum)) do + local start_col, end_col, hl_group = hl[1], hl[2], hl[3] + if end_col == -1 then + end_col = src_line:len() + end + -- If the highlight starts at the beginning of the source line, then it might be off the + -- buffer in the quickfix because we've removed leading whitespace. If so, clamp the value + -- to 0. Except, for some reason 0 gives incorrect results, but -1 works properly even + -- though -1 should indicate the *end* of the line. Not sure why this work, but it does. + local hl_start = math.max(-1, start_col + offset) + vim.api.nvim_buf_set_extmark(qfbufnr, ns, lnum - 1, hl_start, { + hl_group = hl_group, + end_col = end_col + offset, + priority = 100, + strict = false, + }) + end + end + + -- Add LSP semantic token highlights + if config.highlight.lsp then + for _, hl in ipairs(highlight.buf_get_lsp_highlights(item.bufnr, item.lnum)) do + local start_col, end_col, hl_group, priority = hl[1], hl[2], hl[3], hl[4] + vim.api.nvim_buf_set_extmark(qfbufnr, ns, lnum - 1, start_col + offset, { + hl_group = hl_group, + end_col = end_col + offset, + priority = vim.highlight.priorities.semantic_tokens + priority, + strict = false, + }) + end + end + end +end + +---@param qfbufnr integer +---@param info QuickFixTextFuncInfo +local function highlight_buffer_when_entered(qfbufnr, info) + if vim.b[qfbufnr].pending_highlight then + return + end + vim.api.nvim_create_autocmd("BufEnter", { + desc = "Highlight quickfix buffer when entered", + buffer = qfbufnr, + nested = true, + once = true, + callback = function() + vim.b[qfbufnr].pending_highlight = nil + info.start_idx = 1 + info.end_idx = vim.api.nvim_buf_line_count(qfbufnr) + schedule_highlights(info) + end, + }) + vim.b[qfbufnr].pending_highlight = true +end + +---@param info QuickFixTextFuncInfo +---@return {qfbufnr: integer, id: integer, context?: any} +---@overload fun(info: QuickFixTextFuncInfo, all: true): {qfbufnr: integer, id: integer, items: QuickFixItem[], context?: any} +local function load_qf(info, all) + local query + if all then + query = { all = 0 } + else + query = { id = info.id, items = 0, qfbufnr = 0, context = 0 } + end + if info.quickfix == 1 then + return vim.fn.getqflist(query) + else + return vim.fn.getloclist(info.winid, query) + end +end + +---@param info QuickFixTextFuncInfo +add_qf_highlights = function(info) + local qf_list = load_qf(info, true) + local qfbufnr = qf_list.qfbufnr + if not qfbufnr or qfbufnr == 0 then + return + elseif info.end_idx < info.start_idx then + return + end + + local lines = vim.api.nvim_buf_get_lines(qfbufnr, 0, -1, false) + if #lines == 1 and lines[1] == "" then + -- If the quickfix buffer is not visible, it is possible that quickfixtextfunc has run but the + -- buffer has not been populated yet. If that is the case, we should exit early and ensure that + -- the highlighting task runs again when the buffer is opened in a window. + -- see https://github.com/stevearc/quicker.nvim/pull/8 + highlight_buffer_when_entered(qfbufnr, info) + return + end + local ns = vim.api.nvim_create_namespace("quicker_highlights") + + -- Only clear the error namespace during the first pass of "fast" highlighting + if not info.force_bufload then + local err_ns = vim.api.nvim_create_namespace("quicker_err") + vim.api.nvim_buf_clear_namespace(qfbufnr, err_ns, 0, -1) + end + + local start = vim.uv.hrtime() / 1e6 + for i = info.start_idx, info.end_idx do + vim.api.nvim_buf_clear_namespace(qfbufnr, ns, i - 1, i) + ---@type nil|QuickFixItem + local item = qf_list.items[i] + -- If the quickfix list has changed length since the async highlight job has started, + -- we should abort and let the next async highlight task pick it up. + if not item then + return + end + + local line = lines[i] + if not line then + break + end + if item.bufnr ~= 0 then + local loaded = vim.api.nvim_buf_is_loaded(item.bufnr) + if not loaded and info.force_bufload then + vim.fn.bufload(item.bufnr) + loaded = true + end + + if loaded then + add_item_highlights_from_buf(qfbufnr, item, line, i) + elseif config.highlight.treesitter then + local filename = vim.split(line, EM_QUAD, { plain = true })[1] + local offset = filename:len() + EM_QUAD_LEN + local text = line:sub(offset + 1) + for _, hl in ipairs(highlight.get_heuristic_ts_highlights(item, text)) do + local start_col, end_col, hl_group = hl[1], hl[2], hl[3] + start_col = start_col + offset + end_col = end_col + offset + vim.api.nvim_buf_set_extmark(qfbufnr, ns, i - 1, start_col, { + hl_group = hl_group, + end_col = end_col, + priority = 100, + strict = false, + }) + end + end + end + + local user_data = util.get_user_data(item) + -- Set sign if item has a type + if item.type and item.type ~= "" then + local mark = { + sign_text = get_icon(item.type), + sign_hl_group = sign_highlight_map[item.type:upper()], + invalidate = true, + } + if user_data.error_text then + mark.virt_text = { + { user_data.error_text, virt_text_highlight_map[item.type:upper()] or "Normal" }, + } + end + vim.api.nvim_buf_set_extmark(qfbufnr, ns, i - 1, 0, mark) + end + + -- If we've been processing for too long, defer to preserve editor responsiveness + local delta = vim.uv.hrtime() / 1e6 - start + if delta > 50 then + info.start_idx = i + 1 + schedule_highlights(info) + return + end + end + + vim.api.nvim_buf_clear_namespace(qf_list.qfbufnr, ns, info.end_idx, -1) +end + +---@param str string +---@param len integer +---@return string +local function rpad(str, len) + return str .. string.rep(" ", len - vim.api.nvim_strwidth(str)) +end + +---@param items QuickFixItem[] +---@return integer +local function get_lnum_width(items) + local max_len = 2 + local max = 99 + for _, item in ipairs(items) do + if item.lnum > max then + max_len = tostring(item.lnum):len() + max = item.lnum + end + end + return max_len +end + +---@param text string +---@param prefix? string +local function remove_prefix(text, prefix) + local ret + if prefix and prefix ~= "" then + ret = text:sub(prefix:len() + 1) + else + ret = text + end + + return ret +end + +---@class QuickFixTextFuncInfo +---@field id integer +---@field start_idx integer +---@field end_idx integer +---@field winid integer +---@field quickfix 1|0 +---@field force_bufload? boolean field injected by us to control if we're forcing a bufload for the syntax highlighting + +-- TODO when appending to a qflist, the alignment can be thrown off +-- TODO when appending to a qflist, the prefix could mismatch earlier lines +---@param info QuickFixTextFuncInfo +---@return string[] +function M.quickfixtextfunc(info) + local b = config.borders + local qf_list = load_qf(info, true) + local locations = {} + local invalid_filenames = {} + local headers = {} + local ret = {} + local items = qf_list.items + local lnum_width = get_lnum_width(items) + local col_width = get_cached_qf_col_width(info.id, items) + local lnum_fmt = string.format("%%%ds", lnum_width) + local prefixes = calc_whitespace_prefix(items) + local no_filenames = col_width == 0 + + local function get_virt_text(lnum) + -- If none of the quickfix items have filenames, we don't need the lnum column and we only need + -- to show a single delimiter. Technically we don't need any delimiter, but this maintains some + -- of the original qf behavior while being a bit more visually appealing. + if no_filenames then + return { { b.vert, "Delimiter" } } + else + return { + { b.vert, "Delimiter" }, + { lnum_fmt:format(lnum), "QuickFixLineNr" }, + { b.vert, "Delimiter" }, + } + end + end + + for i = info.start_idx, info.end_idx do + local item = items[i] + local user_data = util.get_user_data(item) + + -- First check if there's a header that we need to save to render as virtual text later + if user_data.header == "hard" then + -- Header when expanded QF list + local pieces = { + string.rep(b.strong_header, col_width + 1), + b.strong_cross, + string.rep(b.strong_header, lnum_width), + } + local header_len = config.header_length("hard", col_width + lnum_width + 2) + if header_len > 0 then + table.insert(pieces, b.strong_cross) + table.insert(pieces, string.rep(b.strong_header, header_len)) + else + table.insert(pieces, b.strong_end) + end + table.insert(headers, { i, { { table.concat(pieces, ""), "QuickFixHeaderHard" } } }) + elseif user_data.header == "soft" then + -- Soft header when expanded QF list + local pieces = { + string.rep(b.soft_header, col_width + 1), + b.soft_cross, + string.rep(b.soft_header, lnum_width), + } + local header_len = config.header_length("soft", col_width + lnum_width + 2) + if header_len > 0 then + table.insert(pieces, b.soft_cross) + table.insert(pieces, string.rep(b.soft_header, header_len)) + else + table.insert(pieces, b.soft_end) + end + table.insert(headers, { i, { { table.concat(pieces, ""), "QuickFixHeaderSoft" } } }) + end + + -- Construct the lines and save the filename + lnum to render as virtual text later + local trimmed_text + if config.trim_leading_whitespace == "all" then + trimmed_text = item.text:gsub("^%s*", "") + elseif config.trim_leading_whitespace == "common" then + trimmed_text = remove_prefix(item.text, prefixes[item.bufnr]) + else + trimmed_text = item.text + end + if item.valid == 1 then + -- Matching line + local lnum = item.lnum == 0 and " " or item.lnum + local filename = rpad(M.get_filename_from_item(item), col_width) + table.insert(locations, get_virt_text(lnum)) + table.insert(ret, filename .. EM_QUAD .. trimmed_text) + elseif user_data.lnum then + -- Non-matching line from quicker.nvim context lines + local filename = string.rep(" ", col_width) + table.insert(locations, get_virt_text(user_data.lnum)) + table.insert(ret, filename .. EM_QUAD .. trimmed_text) + else + -- Other non-matching line + local lnum = item.lnum == 0 and " " or item.lnum + local filename = rpad(M.get_filename_from_item(item), col_width) + table.insert(locations, get_virt_text(lnum)) + invalid_filenames[#locations] = true + table.insert(ret, filename .. EM_QUAD .. trimmed_text) + end + end + + -- Render the filename+lnum and the headers as virtual text + local start_idx = info.start_idx + local set_virt_text + set_virt_text = function() + qf_list = load_qf(info) + if qf_list.qfbufnr > 0 then + -- Sometimes the buffer is not fully populated yet. If so, we should try again later. + local num_lines = vim.api.nvim_buf_line_count(qf_list.qfbufnr) + if num_lines < info.end_idx then + vim.schedule(set_virt_text) + return + end + + local ns = vim.api.nvim_create_namespace("quicker_locations") + vim.api.nvim_buf_clear_namespace(qf_list.qfbufnr, ns, start_idx - 1, -1) + local header_ns = vim.api.nvim_create_namespace("quicker_headers") + vim.api.nvim_buf_clear_namespace(qf_list.qfbufnr, header_ns, start_idx - 1, -1) + local filename_ns = vim.api.nvim_create_namespace("quicker_filenames") + vim.api.nvim_buf_clear_namespace(qf_list.qfbufnr, filename_ns, start_idx - 1, -1) + + local idmap = {} + local lines = vim.api.nvim_buf_get_lines(qf_list.qfbufnr, start_idx - 1, -1, false) + for i, loc in ipairs(locations) do + local end_col = lines[i]:find(EM_QUAD, 1, true) or col_width + local lnum = start_idx + i - 1 + local id = + vim.api.nvim_buf_set_extmark(qf_list.qfbufnr, ns, lnum - 1, end_col + EM_QUAD_LEN - 1, { + right_gravity = false, + virt_text = loc, + virt_text_pos = "inline", + invalidate = true, + }) + idmap[id] = lnum + + -- Highlight the filename + vim.api.nvim_buf_set_extmark(qf_list.qfbufnr, filename_ns, lnum - 1, 0, { + hl_group = invalid_filenames[i] and "QuickFixFilenameInvalid" or "QuickFixFilename", + right_gravity = false, + end_col = end_col, + priority = 100, + invalidate = true, + }) + end + vim.b[qf_list.qfbufnr].qf_ext_id_to_item_idx = idmap + + for _, pair in ipairs(headers) do + local i, header = pair[1], pair[2] + local lnum = start_idx + i - 1 + vim.api.nvim_buf_set_extmark(qf_list.qfbufnr, header_ns, lnum - 1, 0, { + virt_lines = { header }, + virt_lines_above = true, + }) + end + end + end + vim.schedule(set_virt_text) + + -- If we just rendered the last item, add highlights + if info.end_idx == #items then + schedule_highlights(info) + + if qf_list.qfbufnr > 0 then + vim.b[qf_list.qfbufnr].qf_prefixes = prefixes + end + end + + return ret +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/editor.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/editor.lua new file mode 100644 index 0000000..3f5db65 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/editor.lua @@ -0,0 +1,405 @@ +local config = require("quicker.config") +local display = require("quicker.display") +local util = require("quicker.util") +local M = {} + +---@class (exact) quicker.ParsedLine +---@field filename? string +---@field lnum? integer +---@field text? string + +---@param n integer +---@param base string +---@param pluralized? string +---@return string +local function plural(n, base, pluralized) + if n == 1 then + return base + elseif pluralized then + return pluralized + else + return base .. "s" + end +end + +---Replace the text in a quickfix line, preserving the lineno virt text +---@param bufnr integer +---@param lnum integer +---@param new_text string +local function replace_qf_line(bufnr, lnum, new_text) + local old_line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1] + + local old_idx = old_line:find(display.EM_QUAD, 1, true) + local new_idx = new_text:find(display.EM_QUAD, 1, true) + + -- If we're missing the em quad delimiter in either the old or new text, the best we can do is + -- replace the whole line + if not old_idx or not new_idx then + vim.api.nvim_buf_set_text(bufnr, lnum - 1, 0, lnum - 1, -1, { new_text }) + return + end + + -- Replace first the text after the em quad, then the filename before. + -- This keeps the line number virtual text in the same location. + vim.api.nvim_buf_set_text( + bufnr, + lnum - 1, + old_idx + display.EM_QUAD_LEN - 1, + lnum - 1, + -1, + { new_text:sub(new_idx + display.EM_QUAD_LEN) } + ) + vim.api.nvim_buf_set_text(bufnr, lnum - 1, 0, lnum - 1, old_idx, { new_text:sub(1, new_idx) }) +end + +---@param bufnr integer +---@param lnum integer +---@param text string +---@param text_hl? string +local function add_qf_error(bufnr, lnum, text, text_hl) + local line = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, true)[1] + local col = line:find(config.borders.vert, 1, true) + if col then + col = line:find(config.borders.vert, col + config.borders.vert:len(), true) + + config.borders.vert:len() + - 1 + else + col = 0 + end + local offset = vim.api.nvim_strwidth(line:sub(1, col)) + local ns = vim.api.nvim_create_namespace("quicker_err") + vim.api.nvim_buf_set_extmark(bufnr, ns, lnum - 1, col, { + virt_text = { { config.type_icons.E, "DiagnosticSignError" } }, + virt_text_pos = "inline", + virt_lines = { + { + { string.rep(" ", offset), "Normal" }, + { "↳ ", "DiagnosticError" }, + { text, text_hl or "Normal" }, + }, + }, + }) +end + +---@param item QuickFixItem +---@param needle string +---@param src_line nil|string +---@return nil|table text_change +---@return nil|string error +local function get_text_edit(item, needle, src_line) + if not src_line then + return nil + elseif item.text == needle then + return nil + elseif src_line ~= item.text then + if item.text:gsub("^%s*", "") == src_line:gsub("^%s*", "") then + -- If they only disagree in their leading whitespace, just take the changes after the + -- whitespace and assume that the whitespace hasn't changed. + -- This can happen if the setqflist caller doesn't use the same whitespace as the source file, + -- for example overseer.nvim Grep will convert tabs to spaces because the embedded terminal + -- will convert tabs to spaces. + needle = src_line:match("^%s*") .. needle:gsub("^%s*", "") + else + return nil, "buffer text does not match source text" + end + end + + return { + newText = needle, + range = { + start = { + line = item.lnum - 1, + character = 0, + }, + ["end"] = { + line = item.lnum - 1, + character = #src_line, + }, + }, + } +end + +---Deserialize qf_prefixes from the buffer, converting vim.NIL to nil +---@param bufnr integer +---@return table<integer, string> +local function load_qf_prefixes(bufnr) + local prefixes = vim.b[bufnr].qf_prefixes or {} + for k, v in pairs(prefixes) do + if v == vim.NIL then + prefixes[k] = nil + end + end + return prefixes +end + +---@param bufnr integer +---@param loclist_win? integer +local function save_changes(bufnr, loclist_win) + if not vim.bo[bufnr].modified then + return + end + local ns = vim.api.nvim_create_namespace("quicker_err") + vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1) + local qf_list + if loclist_win then + qf_list = vim.fn.getloclist(loclist_win, { all = 0 }) + else + qf_list = vim.fn.getqflist({ all = 0 }) + end + + local changes = {} + local function add_change(buf, text_edit) + if not changes[buf] then + changes[buf] = {} + end + local last_edit = changes[buf][#changes[buf]] + if last_edit and vim.deep_equal(last_edit.range, text_edit.range) then + if last_edit.newText == text_edit.newText then + return + else + return "conflicting changes on the same line" + end + end + table.insert(changes[buf], text_edit) + end + + -- Parse the buffer + local winid = util.buf_find_win(bufnr) + local new_items = {} + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) + local errors = {} + local exit_early = false + local prefixes = load_qf_prefixes(bufnr) + local ext_id_to_item_idx = vim.b[bufnr].qf_ext_id_to_item_idx + for i, line in ipairs(lines) do + (function() + local extmarks = util.get_lnum_extmarks(bufnr, i, line:len()) + assert(#extmarks <= 1, string.format("Found more than one extmark on line %d", i)) + local found_idx + if extmarks[1] then + found_idx = ext_id_to_item_idx[extmarks[1][1]] + end + + -- If we didn't find a match, the line was most likely added or reordered + if not found_idx then + add_qf_error( + bufnr, + i, + "quicker.nvim does not support adding or reordering quickfix items", + "DiagnosticError" + ) + if winid then + vim.api.nvim_win_set_cursor(winid, { i, 0 }) + end + exit_early = true + return + end + + -- Trim the filename off of the line + local idx = string.find(line, display.EM_QUAD, 1, true) + if not idx then + add_qf_error( + bufnr, + i, + "The delimiter between filename and text has been deleted. Undo, delete line, or :Refresh.", + "DiagnosticError" + ) + if winid then + vim.api.nvim_win_set_cursor(winid, { i, 0 }) + end + exit_early = true + return + end + local text = line:sub(idx + display.EM_QUAD_LEN) + + local item = qf_list.items[found_idx] + if item.bufnr ~= 0 and item.lnum ~= 0 then + if not vim.api.nvim_buf_is_loaded(item.bufnr) then + vim.fn.bufload(item.bufnr) + end + local src_line = vim.api.nvim_buf_get_lines(item.bufnr, item.lnum - 1, item.lnum, false)[1] + + -- add the whitespace prefix back to the parsed line text + if config.trim_leading_whitespace == "common" then + text = (prefixes[item.bufnr] or "") .. text + elseif config.trim_leading_whitespace == "all" and src_line then + text = src_line:match("^%s*") .. text + end + + if src_line and text ~= src_line then + if text:gsub("^%s*", "") == src_line:gsub("^%s*", "") then + -- If they only disagree in their leading whitespace, just take the changes after the + -- whitespace and assume that the whitespace hasn't changed + text = src_line:match("^%s*") .. text:gsub("^%s*", "") + end + end + + local text_edit, err = get_text_edit(item, text, src_line) + if text_edit then + local chng_err = add_change(item.bufnr, text_edit) + if chng_err then + add_qf_error(bufnr, i, chng_err, "DiagnosticError") + if winid then + vim.api.nvim_win_set_cursor(winid, { i, 0 }) + end + exit_early = true + return + end + elseif err then + table.insert(new_items, item) + errors[#new_items] = line + return + end + end + + -- add item to future qflist + item.text = text + table.insert(new_items, item) + end)() + if exit_early then + vim.schedule(function() + vim.bo[bufnr].modified = true + end) + return + end + end + + ---@type table<integer, boolean> + local buf_was_modified = {} + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + buf_was_modified[buf] = vim.bo[buf].modified + end + local autosave = config.edit.autosave + local num_applied = 0 + local modified_bufs = {} + for chg_buf, text_edits in pairs(changes) do + modified_bufs[chg_buf] = true + num_applied = num_applied + #text_edits + vim.lsp.util.apply_text_edits(text_edits, chg_buf, "utf-8") + local was_modified = buf_was_modified[chg_buf] + local should_save = autosave == true or (autosave == "unmodified" and not was_modified) + -- Autosave changed buffers if they were not modified before + if should_save then + vim.api.nvim_buf_call(chg_buf, function() + vim.cmd.update({ mods = { emsg_silent = true, noautocmd = true } }) + end) + end + end + if num_applied > 0 then + local num_files = vim.tbl_count(modified_bufs) + local num_errors = vim.tbl_count(errors) + if num_errors > 0 then + local total = num_errors + num_applied + vim.notify( + string.format( + "Applied %d/%d %s in %d %s", + num_applied, + total, + plural(total, "change"), + num_files, + plural(num_files, "file") + ), + vim.log.levels.WARN + ) + else + vim.notify( + string.format( + "Applied %d %s in %d %s", + num_applied, + plural(num_applied, "change"), + num_files, + plural(num_files, "file") + ), + vim.log.levels.INFO + ) + end + end + + local view + if winid then + view = vim.api.nvim_win_call(winid, function() + return vim.fn.winsaveview() + end) + end + if loclist_win then + vim.fn.setloclist( + loclist_win, + {}, + "r", + { items = new_items, title = qf_list.title, context = qf_list.context } + ) + else + vim.fn.setqflist( + {}, + "r", + { items = new_items, title = qf_list.title, context = qf_list.context } + ) + end + if winid and view then + vim.api.nvim_win_call(winid, function() + vim.fn.winrestview(view) + end) + end + + -- Schedule this so it runs after the save completes, and the buffer will be correctly marked as modified + if not vim.tbl_isempty(errors) then + vim.schedule(function() + -- Mark the lines with changes that could not be applied + for lnum, new_text in pairs(errors) do + replace_qf_line(bufnr, lnum, new_text) + local item = new_items[lnum] + local src_line = vim.api.nvim_buf_get_lines(item.bufnr, item.lnum - 1, item.lnum, false)[1] + add_qf_error(bufnr, lnum, src_line) + if winid and vim.api.nvim_win_is_valid(winid) then + vim.api.nvim_win_set_cursor(winid, { lnum, 0 }) + end + end + end) + + -- Notify user that some changes could not be applied + local cnt = vim.tbl_count(errors) + local change_text = cnt == 1 and "change" or "changes" + vim.notify( + string.format( + "%d %s could not be applied due to conflicts in the source buffer. Please :Refresh and try again.", + cnt, + change_text + ), + vim.log.levels.ERROR + ) + end +end + +-- TODO add support for undo past last change + +---@param bufnr integer +function M.setup_editor(bufnr) + local aug = vim.api.nvim_create_augroup("quicker", { clear = false }) + local loclist_win + vim.api.nvim_buf_call(bufnr, function() + local ok, qf = pcall(vim.fn.getloclist, 0, { filewinid = 0 }) + if ok and qf.filewinid and qf.filewinid ~= 0 then + loclist_win = qf.filewinid + end + end) + + -- Set a name for the buffer so we can save it + local bufname = string.format("quickfix-%d", bufnr) + if vim.api.nvim_buf_get_name(bufnr) == "" then + vim.api.nvim_buf_set_name(bufnr, bufname) + end + vim.bo[bufnr].modifiable = true + + vim.api.nvim_create_autocmd("BufWriteCmd", { + desc = "quicker.nvim apply changes on write", + group = aug, + buffer = bufnr, + nested = true, + callback = function(args) + save_changes(args.buf, loclist_win) + vim.bo[args.buf].modified = false + end, + }) +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/follow.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/follow.lua new file mode 100644 index 0000000..d5500a2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/follow.lua @@ -0,0 +1,84 @@ +local util = require("quicker.util") +local M = {} + +M.seek_to_position = function() + if util.is_open(0) then + local qf_list = vim.fn.getloclist(0, { winid = 0, items = 0 }) + local new_pos = M.calculate_pos(qf_list.items) + if new_pos then + M.set_pos(qf_list.winid, new_pos) + end + end + + if util.is_open() then + local qf_list = vim.fn.getqflist({ winid = 0, items = 0 }) + local new_pos = M.calculate_pos(qf_list.items) + if new_pos then + M.set_pos(qf_list.winid, new_pos) + end + end +end + +---Calculate the current buffer/cursor location in the quickfix list +---@param list QuickFixItem[] +---@return nil|integer +M.calculate_pos = function(list) + if vim.bo.buftype ~= "" then + return + end + local bufnr = vim.api.nvim_get_current_buf() + local cursor = vim.api.nvim_win_get_cursor(0) + local lnum, col = cursor[1], cursor[2] + 1 + local prev_lnum = -1 + local prev_col = -1 + local found_buf = false + local ret + for i, entry in ipairs(list) do + if entry.bufnr ~= bufnr then + if found_buf then + return ret + end + else + found_buf = true + + -- If we detect that the list isn't sorted, bail. + if + prev_lnum > -1 + and (entry.lnum < prev_lnum or (entry.lnum == prev_lnum and entry.col <= prev_col)) + then + return + end + + if prev_lnum == -1 or lnum > entry.lnum or (lnum == entry.lnum and col >= entry.col) then + ret = i + end + prev_lnum = entry.lnum + prev_col = entry.col + end + end + + return ret +end + +local timers = {} +---@param winid integer +---@param pos integer +M.set_pos = function(winid, pos) + local timer = timers[winid] + if timer then + timer:close() + end + timer = assert(vim.uv.new_timer()) + timers[winid] = timer + timer:start(10, 0, function() + timer:close() + timers[winid] = nil + vim.schedule(function() + if vim.api.nvim_win_is_valid(winid) then + pcall(vim.api.nvim_win_set_cursor, winid, { pos, 0 }) + end + end) + end) +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/fs.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/fs.lua new file mode 100644 index 0000000..f0e94dc --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/fs.lua @@ -0,0 +1,98 @@ +local M = {} + +---@type boolean +M.is_windows = vim.uv.os_uname().version:match("Windows") + +M.is_mac = vim.uv.os_uname().sysname == "Darwin" + +M.is_linux = not M.is_windows and not M.is_mac + +---@type string +M.sep = M.is_windows and "\\" or "/" + +---@param ... string +M.join = function(...) + return table.concat({ ... }, M.sep) +end + +---Check if OS path is absolute +---@param dir string +---@return boolean +M.is_absolute = function(dir) + if M.is_windows then + return dir:match("^%a:\\") + else + return vim.startswith(dir, "/") + end +end + +M.abspath = function(path) + if not M.is_absolute(path) then + path = vim.fn.fnamemodify(path, ":p") + end + return path +end + +local home_dir = assert(vim.uv.os_homedir()) + +---@param path string +---@param relative_to? string Shorten relative to this path (default cwd) +---@return string +M.shorten_path = function(path, relative_to) + if not relative_to then + relative_to = vim.fn.getcwd() + end + local relpath + if M.is_subpath(relative_to, path) then + local idx = relative_to:len() + 1 + -- Trim the dividing slash if it's not included in relative_to + if not vim.endswith(relative_to, "/") and not vim.endswith(relative_to, "\\") then + idx = idx + 1 + end + relpath = path:sub(idx) + if relpath == "" then + relpath = "." + end + end + if M.is_subpath(home_dir, path) then + local homepath = "~" .. path:sub(home_dir:len() + 1) + if not relpath or homepath:len() < relpath:len() then + return homepath + end + end + return relpath or path +end + +--- Returns true if candidate is a subpath of root, or if they are the same path. +---@param root string +---@param candidate string +---@return boolean +M.is_subpath = function(root, candidate) + if candidate == "" then + return false + end + root = vim.fs.normalize(M.abspath(root)) + -- Trim trailing "/" from the root + if root:find("/", -1) then + root = root:sub(1, -2) + end + candidate = vim.fs.normalize(M.abspath(candidate)) + if M.is_windows then + root = root:lower() + candidate = candidate:lower() + end + if root == candidate then + return true + end + local prefix = candidate:sub(1, root:len()) + if prefix ~= root then + return false + end + + local candidate_starts_with_sep = candidate:find("/", root:len() + 1, true) == root:len() + 1 + local root_ends_with_sep = root:find("/", root:len(), true) == root:len() + + return candidate_starts_with_sep or root_ends_with_sep +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/highlight.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/highlight.lua new file mode 100644 index 0000000..a7323aa --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/highlight.lua @@ -0,0 +1,222 @@ +local M = {} + +---@class quicker.TSHighlight +---@field [1] integer start_col +---@field [2] integer end_col +---@field [3] string highlight group + +local _cached_queries = {} +---@param lang string +---@return vim.treesitter.Query? +local function get_highlight_query(lang) + local query = _cached_queries[lang] + if query == nil then + query = vim.treesitter.query.get(lang, "highlights") or false + _cached_queries[lang] = query + end + if query then + return query + end +end + +---@param bufnr integer +---@param lnum integer +---@return quicker.TSHighlight[] +function M.buf_get_ts_highlights(bufnr, lnum) + local filetype = vim.bo[bufnr].filetype + if not filetype or filetype == "" then + filetype = vim.filetype.match({ buf = bufnr }) or "" + end + local lang = vim.treesitter.language.get_lang(filetype) or filetype + if lang == "" then + return {} + end + local ok, parser = pcall(vim.treesitter.get_parser, bufnr, lang) + if not ok or not parser then + return {} + end + + local row = lnum - 1 + if not parser:is_valid() then + parser:parse(true) + end + + local highlights = {} + parser:for_each_tree(function(tstree, tree) + if not tstree then + return + end + + local root_node = tstree:root() + local root_start_row, _, root_end_row, _ = root_node:range() + + -- Only worry about trees within the line range + if root_start_row > row or root_end_row < row then + return + end + + local query = get_highlight_query(tree:lang()) + + -- Some injected languages may not have highlight queries. + if not query then + return + end + + for capture, node, metadata in query:iter_captures(root_node, bufnr, row, root_end_row + 1) do + if capture == nil then + break + end + + local range = vim.treesitter.get_range(node, bufnr, metadata[capture]) + local start_row, start_col, _, end_row, end_col, _ = unpack(range) + if start_row > row then + break + end + local capture_name = query.captures[capture] + local hl = string.format("@%s.%s", capture_name, tree:lang()) + if end_row > start_row then + end_col = -1 + end + table.insert(highlights, { start_col, end_col, hl }) + end + end) + + return highlights +end + +---@class quicker.LSPHighlight +---@field [1] integer start_col +---@field [2] integer end_col +---@field [3] string highlight group +---@field [4] integer priority modifier + +-- We're accessing private APIs here. This could break in the future. +local STHighlighter = vim.lsp.semantic_tokens.__STHighlighter + +--- Copied from Neovim semantic_tokens.lua +--- Do a binary search of the tokens in the half-open range [lo, hi). +--- +--- Return the index i in range such that tokens[j].line < line for all j < i, and +--- tokens[j].line >= line for all j >= i, or return hi if no such index is found. +--- +---@private +local function lower_bound(tokens, line, lo, hi) + while lo < hi do + local mid = bit.rshift(lo + hi, 1) -- Equivalent to floor((lo + hi) / 2). + if tokens[mid].line < line then + lo = mid + 1 + else + hi = mid + end + end + return lo +end + +---@param bufnr integer +---@param lnum integer +---@return quicker.LSPHighlight[] +function M.buf_get_lsp_highlights(bufnr, lnum) + local highlighter = STHighlighter.active[bufnr] + if not highlighter then + return {} + end + local ft = vim.bo[bufnr].filetype + + local lsp_highlights = {} + for _, client in pairs(highlighter.client_state) do + local highlights = client.current_result.highlights + if highlights then + local idx = lower_bound(highlights, lnum - 1, 1, #highlights + 1) + for i = idx, #highlights do + local token = highlights[i] + + if token.line >= lnum then + break + end + + table.insert( + lsp_highlights, + { token.start_col, token.end_col, string.format("@lsp.type.%s.%s", token.type, ft), 0 } + ) + for modifier, _ in pairs(token.modifiers) do + table.insert( + lsp_highlights, + { token.start_col, token.end_col, string.format("@lsp.mod.%s.%s", modifier, ft), 1 } + ) + table.insert(lsp_highlights, { + token.start_col, + token.end_col, + string.format("@lsp.typemod.%s.%s.%s", token.type, modifier, ft), + 2, + }) + end + end + end + end + + return lsp_highlights +end + +---@param item QuickFixItem +---@param line string +---@return quicker.TSHighlight[] +M.get_heuristic_ts_highlights = function(item, line) + local filetype = vim.filetype.match({ buf = item.bufnr }) + if not filetype then + return {} + end + + local lang = vim.treesitter.language.get_lang(filetype) + if not lang then + return {} + end + + local has_parser, parser = pcall(vim.treesitter.get_string_parser, line, lang) + if not has_parser then + return {} + end + + local root = parser:parse(true)[1]:root() + local query = vim.treesitter.query.get(lang, "highlights") + if not query then + return {} + end + + local highlights = {} + for capture, node, metadata in query:iter_captures(root, line) do + if capture == nil then + break + end + + local range = vim.treesitter.get_range(node, line, metadata[capture]) + local start_row, start_col, _, end_row, end_col, _ = unpack(range) + local capture_name = query.captures[capture] + local hl = string.format("@%s.%s", capture_name, lang) + if end_row > start_row then + end_col = -1 + end + table.insert(highlights, { start_col, end_col, hl }) + end + + return highlights +end + +function M.set_highlight_groups() + if vim.tbl_isempty(vim.api.nvim_get_hl(0, { name = "QuickFixHeaderHard" })) then + vim.api.nvim_set_hl(0, "QuickFixHeaderHard", { link = "Delimiter", default = true }) + end + if vim.tbl_isempty(vim.api.nvim_get_hl(0, { name = "QuickFixHeaderSoft" })) then + vim.api.nvim_set_hl(0, "QuickFixHeaderSoft", { link = "Comment", default = true }) + end + if vim.tbl_isempty(vim.api.nvim_get_hl(0, { name = "QuickFixFilename" })) then + vim.api.nvim_set_hl(0, "QuickFixFilename", { link = "Directory", default = true }) + end + if vim.tbl_isempty(vim.api.nvim_get_hl(0, { name = "QuickFixFilenameInvalid" })) then + vim.api.nvim_set_hl(0, "QuickFixFilenameInvalid", { link = "Comment", default = true }) + end + if vim.tbl_isempty(vim.api.nvim_get_hl(0, { name = "QuickFixLineNr" })) then + vim.api.nvim_set_hl(0, "QuickFixLineNr", { link = "LineNr", default = true }) + end +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/init.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/init.lua new file mode 100644 index 0000000..42ae32b --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/init.lua @@ -0,0 +1,189 @@ +local M = {} + +---@param opts? quicker.SetupOptions +local function setup(opts) + local config = require("quicker.config") + config.setup(opts) + + local aug = vim.api.nvim_create_augroup("quicker", { clear = true }) + vim.api.nvim_create_autocmd("FileType", { + pattern = "qf", + group = aug, + desc = "quicker.nvim set up quickfix mappings", + callback = function(args) + require("quicker.highlight").set_highlight_groups() + require("quicker.opts").set_opts(args.buf) + require("quicker.keys").set_keymaps(args.buf) + vim.api.nvim_buf_create_user_command(args.buf, "Refresh", function() + require("quicker.context").refresh() + end, { + desc = "Update the quickfix list with the current buffer text for each item", + }) + + if config.constrain_cursor then + require("quicker.cursor").constrain_cursor(args.buf) + end + + config.on_qf(args.buf) + end, + }) + vim.api.nvim_create_autocmd("ColorScheme", { + pattern = "*", + group = aug, + desc = "quicker.nvim set up quickfix highlight groups", + callback = function() + require("quicker.highlight").set_highlight_groups() + end, + }) + if config.edit.enabled then + vim.api.nvim_create_autocmd("BufReadPost", { + pattern = "quickfix", + group = aug, + desc = "quicker.nvim set up quickfix editing", + callback = function(args) + require("quicker.editor").setup_editor(args.buf) + end, + }) + end + if config.follow.enabled then + vim.api.nvim_create_autocmd({ "CursorMoved", "BufEnter" }, { + desc = "quicker.nvim scroll to nearest location in quickfix", + pattern = "*", + group = aug, + callback = function() + require("quicker.follow").seek_to_position() + end, + }) + end + + vim.o.quickfixtextfunc = "v:lua.require'quicker.display'.quickfixtextfunc" + + -- If the quickfix/loclist is already open, refresh it so the quickfixtextfunc will take effect. + -- This is required for lazy-loading to work properly. + local list = vim.fn.getqflist({ all = 0 }) + if not vim.tbl_isempty(list.items) then + vim.fn.setqflist({}, "r", list) + end + for _, winid in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_win_is_valid(winid) then + local llist = vim.fn.getloclist(winid, { all = 0 }) + if not vim.tbl_isempty(list.items) then + vim.fn.setloclist(winid, {}, "r", llist) + end + end + end +end + +M.setup = setup + +---Expand the context around the quickfix results. +---@param opts? quicker.ExpandOpts +---@note +--- If there are multiple quickfix items for the same line of a file, only the first +--- one will remain after calling expand(). +M.expand = function(opts) + return require("quicker.context").expand(opts) +end + +---Collapse the context around quickfix results, leaving only the `valid` items. +M.collapse = function() + return require("quicker.context").collapse() +end + +---Toggle the expanded context around the quickfix results. +---@param opts? quicker.ExpandOpts +M.toggle_expand = function(opts) + return require("quicker.context").toggle(opts) +end + +---Update the quickfix list with the current buffer text for each item. +---@param loclist_win? integer +---@param opts? quicker.RefreshOpts +M.refresh = function(loclist_win, opts) + return require("quicker.context").refresh(loclist_win, opts) +end + +---@param loclist_win? integer Check if loclist is open for the given window. If nil, check quickfix. +M.is_open = function(loclist_win) + return require("quicker.util").is_open(loclist_win) +end + +---@class quicker.OpenCmdMods: vim.api.keyset.parse_cmd.mods + +---@class (exact) quicker.OpenOpts +---@field loclist? boolean Toggle the loclist instead of the quickfix list +---@field focus? boolean Focus the quickfix window after toggling (default false) +---@field height? integer Height of the quickfix window when opened. Defaults to number of items in the list. +---@field min_height? integer Minimum height of the quickfix window. Default 4. +---@field max_height? integer Maximum height of the quickfix window. Default 10. +---@field open_cmd_mods? quicker.OpenCmdMods A table of modifiers for the quickfix or loclist open commands. + +---Toggle the quickfix or loclist window. +---@param opts? quicker.OpenOpts +M.toggle = function(opts) + ---@type {loclist: boolean, focus: boolean, height?: integer, min_height: integer, max_height: integer, open_cmd_mods?: quicker.OpenCmdMods} + opts = vim.tbl_deep_extend("keep", opts or {}, { + loclist = false, + focus = false, + min_height = 4, + max_height = 10, + open_cmd_mods = {}, + }) + local loclist_win = opts.loclist and 0 or nil + if M.is_open(loclist_win) then + M.close({ loclist = opts.loclist }) + else + M.open(opts) + end +end + +---Open the quickfix or loclist window. +---@param opts? quicker.OpenOpts +M.open = function(opts) + local util = require("quicker.util") + ---@type {loclist: boolean, focus: boolean, height?: integer, min_height: integer, max_height: integer, open_cmd_mods?: quicker.OpenCmdMods} + opts = vim.tbl_deep_extend("keep", opts or {}, { + loclist = false, + focus = false, + min_height = 4, + max_height = 10, + open_cmd_mods = {}, + }) + local height + if opts.loclist then + local ok, err = pcall(vim.cmd.lopen, { mods = opts.open_cmd_mods }) + if not ok then + vim.notify(err, vim.log.levels.ERROR) + return + end + height = #vim.fn.getloclist(0) + else + vim.cmd.copen({ mods = opts.open_cmd_mods }) + height = #vim.fn.getqflist() + end + + -- only set the height if the quickfix is not a full-height vsplit + if not util.is_full_height_vsplit(0) then + height = math.min(opts.max_height, math.max(opts.min_height, height)) + vim.api.nvim_win_set_height(0, height) + end + + if not opts.focus then + vim.cmd.wincmd({ args = { "p" } }) + end +end + +---@class (exact) quicker.CloseOpts +---@field loclist? boolean Close the loclist instead of the quickfix list + +---Close the quickfix or loclist window. +---@param opts? quicker.CloseOpts +M.close = function(opts) + if opts and opts.loclist then + vim.cmd.lclose() + else + vim.cmd.cclose() + end +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/keys.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/keys.lua new file mode 100644 index 0000000..17ea331 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/keys.lua @@ -0,0 +1,20 @@ +local config = require("quicker.config") + +local M = {} + +---@param bufnr integer +function M.set_keymaps(bufnr) + for _, defn in ipairs(config.keys) do + vim.keymap.set(defn.mode or "n", defn[1], defn[2], { + buffer = bufnr, + desc = defn.desc, + expr = defn.expr, + nowait = defn.nowait, + remap = defn.remap, + replace_keycodes = defn.replace_keycodes, + silent = defn.silent, + }) + end +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/opts.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/opts.lua new file mode 100644 index 0000000..1cf77b6 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/opts.lua @@ -0,0 +1,61 @@ +local config = require("quicker.config") +local util = require("quicker.util") + +local M = {} + +---@param bufnr integer +local function set_buf_opts(bufnr) + for k, v in pairs(config.opts) do + local opt_info = vim.api.nvim_get_option_info2(k, {}) + if opt_info.scope == "buf" then + local ok, err = pcall(vim.api.nvim_set_option_value, k, v, { buf = bufnr }) + if not ok then + vim.notify( + string.format("Error setting quickfix option %s = %s: %s", k, vim.inspect(v), err), + vim.log.levels.ERROR + ) + end + end + end +end + +---@param winid integer +local function set_win_opts(winid) + for k, v in pairs(config.opts) do + local opt_info = vim.api.nvim_get_option_info2(k, {}) + if opt_info.scope == "win" then + local ok, err = pcall(vim.api.nvim_set_option_value, k, v, { scope = "local", win = winid }) + if not ok then + vim.notify( + string.format("Error setting quickfix window option %s = %s: %s", k, vim.inspect(v), err), + vim.log.levels.ERROR + ) + end + end + end +end + +---@param bufnr integer +function M.set_opts(bufnr) + set_buf_opts(bufnr) + local winid = util.buf_find_win(bufnr) + if winid then + set_win_opts(winid) + else + local aug = vim.api.nvim_create_augroup("quicker", { clear = false }) + vim.api.nvim_create_autocmd("BufWinEnter", { + desc = "Set quickfix window options", + buffer = bufnr, + group = aug, + callback = function() + winid = util.buf_find_win(bufnr) + if winid then + set_win_opts(winid) + end + return winid ~= nil + end, + }) + end +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/util.lua b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/util.lua new file mode 100644 index 0000000..3794091 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/lua/quicker/util.lua @@ -0,0 +1,95 @@ +local M = {} + +---@param bufnr integer +---@return nil|integer +function M.buf_find_win(bufnr) + for _, winid in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_win_is_valid(winid) and vim.api.nvim_win_get_buf(winid) == bufnr then + return winid + end + end +end + +---@param loclist_win? integer Check if loclist is open for the given window. If nil, check quickfix. +M.is_open = function(loclist_win) + if loclist_win then + return vim.fn.getloclist(loclist_win or 0, { winid = 0 }).winid ~= 0 + else + return vim.fn.getqflist({ winid = 0 }).winid ~= 0 + end +end + +---@param winid nil|integer +---@return nil|"c"|"l" +M.get_win_type = function(winid) + if not winid or winid == 0 then + winid = vim.api.nvim_get_current_win() + end + local info = vim.fn.getwininfo(winid)[1] + if info.quickfix == 0 then + return nil + elseif info.loclist == 0 then + return "c" + else + return "l" + end +end + +---@param item QuickFixItem +---@return QuickFixUserData +M.get_user_data = function(item) + if type(item.user_data) == "table" then + return item.user_data + else + return {} + end +end + +---Get valid location extmarks for a line in the quickfix +---@param bufnr integer +---@param lnum integer +---@param line_len? integer how long this particular line is +---@param ns? integer namespace of extmarks +---@return table[] extmarks +M.get_lnum_extmarks = function(bufnr, lnum, line_len, ns) + if not ns then + ns = vim.api.nvim_create_namespace("quicker_locations") + end + if not line_len then + local line = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, true)[1] + line_len = line:len() + end + local extmarks = vim.api.nvim_buf_get_extmarks( + bufnr, + ns, + { lnum - 1, 0 }, + { lnum - 1, line_len }, + { details = true } + ) + return vim.tbl_filter(function(mark) + return not mark[4].invalid + end, extmarks) +end + +---Return true if the window is a full-height leaf window +---@param winid? integer +---@return boolean +M.is_full_height_vsplit = function(winid) + if not winid or winid == 0 then + winid = vim.api.nvim_get_current_win() + end + local layout = vim.fn.winlayout() + -- If the top layout is not vsplit, then it's not a vertical leaf + if layout[1] ~= "row" then + return false + end + for _, v in ipairs(layout[2]) do + if v[1] == "leaf" and v[2] == winid then + return true + end + end + + return false +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/run_tests.sh b/mut/neovim/pack/plugins/start/quicker.nvim/run_tests.sh new file mode 100755 index 0000000..f7b5bab --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/run_tests.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +for arg in "$@"; do + shift + case "$arg" in + '--update') + export UPDATE_SNAPSHOTS=1 + ;; + *) + set -- "$@" "$arg" + ;; + esac +done + +mkdir -p ".testenv/config/nvim" +mkdir -p ".testenv/data/nvim" +mkdir -p ".testenv/state/nvim" +mkdir -p ".testenv/run/nvim" +mkdir -p ".testenv/cache/nvim" +PLUGINS=".testenv/data/nvim/site/pack/plugins/start" + +if [ ! -e "$PLUGINS/plenary.nvim" ]; then + git clone --depth=1 https://github.com/nvim-lua/plenary.nvim.git "$PLUGINS/plenary.nvim" +else + (cd "$PLUGINS/plenary.nvim" && git pull) +fi + +XDG_CONFIG_HOME=".testenv/config" \ + XDG_DATA_HOME=".testenv/data" \ + XDG_STATE_HOME=".testenv/state" \ + XDG_RUNTIME_DIR=".testenv/run" \ + XDG_CACHE_HOME=".testenv/cache" \ + nvim --headless -u tests/minimal_init.lua \ + -c "RunTests ${1-tests}" +echo "Success" diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/scripts/generate.py b/mut/neovim/pack/plugins/start/quicker.nvim/scripts/generate.py new file mode 100755 index 0000000..e1ccded --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/scripts/generate.py @@ -0,0 +1,102 @@ +import os +import os.path +import re +from typing import List + +from nvim_doc_tools import ( + Vimdoc, + VimdocSection, + generate_md_toc, + indent, + parse_directory, + read_section, + render_md_api2, + render_vimdoc_api2, + replace_section, +) + +HERE = os.path.dirname(__file__) +ROOT = os.path.abspath(os.path.join(HERE, os.path.pardir)) +README = os.path.join(ROOT, "README.md") +DOC = os.path.join(ROOT, "doc") +VIMDOC = os.path.join(DOC, "quicker.txt") + + +def add_md_link_path(path: str, lines: List[str]) -> List[str]: + ret = [] + for line in lines: + ret.append(re.sub(r"(\(#)", "(" + path + "#", line)) + return ret + + +def update_md_api(): + types = parse_directory(os.path.join(ROOT, "lua")) + funcs = types.files["quicker/init.lua"].functions + lines = ["\n"] + render_md_api2(funcs, types, 3)[:-1] # trim last newline + replace_section( + README, + r"^<!-- API -->$", + r"^<!-- /API -->$", + lines, + ) + + +def update_options(): + option_lines = ["\n", "```lua\n"] + config_file = os.path.join(ROOT, "lua", "quicker", "config.lua") + option_lines = read_section(config_file, r"^\s*local default_config =", r"^}$") + option_lines.insert(0, 'require("quicker").setup({\n') + option_lines.insert(0, "```lua\n") + option_lines.extend(["})\n", "```\n", "\n"]) + replace_section( + README, + r"^<!-- OPTIONS -->$", + r"^<!-- /OPTIONS -->$", + option_lines, + ) + + +def update_readme_toc(): + toc = ["\n"] + generate_md_toc(README, max_level=1) + ["\n"] + replace_section( + README, + r"^<!-- TOC -->$", + r"^<!-- /TOC -->$", + toc, + ) + + +def gen_options_vimdoc() -> VimdocSection: + section = VimdocSection("Options", "quicker-options", ["\n", ">lua\n"]) + config_file = os.path.join(ROOT, "lua", "quicker", "config.lua") + option_lines = read_section(config_file, r"^\s*local default_config =", r"^}$") + option_lines.insert(0, 'require("quicker").setup({\n') + option_lines.extend(["})\n"]) + section.body.extend(indent(option_lines, 4)) + section.body.append("<\n") + return section + + +def generate_vimdoc(): + doc = Vimdoc("quicker.txt", "quicker") + types = parse_directory(os.path.join(ROOT, "lua")) + funcs = types.files["quicker/init.lua"].functions + doc.sections.extend( + [ + gen_options_vimdoc(), + VimdocSection( + "API", "quicker-api", render_vimdoc_api2("quicker", funcs, types) + ), + ] + ) + + with open(VIMDOC, "w", encoding="utf-8") as ofile: + ofile.writelines(doc.render()) + + +def main() -> None: + """Update the README""" + update_md_api() + update_options() + update_readme_toc() + generate_vimdoc() diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/scripts/main.py b/mut/neovim/pack/plugins/start/quicker.nvim/scripts/main.py new file mode 100755 index 0000000..4dffddf --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/scripts/main.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +import argparse +import os +import sys + +HERE = os.path.dirname(__file__) +ROOT = os.path.abspath(os.path.join(HERE, os.path.pardir)) +DOC = os.path.join(ROOT, "doc") + + +def main() -> None: + """Generate docs""" + sys.path.append(HERE) + parser = argparse.ArgumentParser(description=main.__doc__) + parser.add_argument("command", choices=["generate", "lint"]) + args = parser.parse_args() + if args.command == "generate": + import generate + + generate.main() + elif args.command == "lint": + from nvim_doc_tools import lint_md_links + + files = [os.path.join(ROOT, "README.md")] + [ + os.path.join(DOC, file) for file in os.listdir(DOC) if file.endswith(".md") + ] + lint_md_links.main(ROOT, files) + + +if __name__ == "__main__": + main() diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/scripts/requirements.txt b/mut/neovim/pack/plugins/start/quicker.nvim/scripts/requirements.txt new file mode 100644 index 0000000..2c6271f --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/scripts/requirements.txt @@ -0,0 +1,4 @@ +pyparsing==3.0.9 +black +isort +mypy diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/syntax/qf.vim b/mut/neovim/pack/plugins/start/quicker.nvim/syntax/qf.vim new file mode 100644 index 0000000..8a19536 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/syntax/qf.vim @@ -0,0 +1,7 @@ +if exists('b:current_syntax') + finish +endif + +syn match QuickFixText /^.*/ + +let b:current_syntax = 'qf' diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/context_spec.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/context_spec.lua new file mode 100644 index 0000000..4a4ef5a --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/context_spec.lua @@ -0,0 +1,134 @@ +local quicker = require("quicker") +local test_util = require("tests.test_util") + +describe("context", function() + after_each(function() + test_util.reset_editor() + end) + + it("expand results", function() + local first = test_util.make_tmp_file("expand_1.txt", 10) + local second = test_util.make_tmp_file("expand_2.txt", 10) + local first_buf = vim.fn.bufadd(first) + local second_buf = vim.fn.bufadd(second) + vim.fn.setqflist({ + { + bufnr = first_buf, + text = "line 2", + lnum = 2, + valid = 1, + }, + { + bufnr = first_buf, + text = "line 8", + lnum = 8, + valid = 1, + }, + { + bufnr = second_buf, + text = "line 4", + lnum = 4, + valid = 1, + }, + }) + vim.cmd.copen() + test_util.assert_snapshot(0, "expand_1") + + vim.api.nvim_win_set_cursor(0, { 3, 0 }) + quicker.expand() + test_util.assert_snapshot(0, "expand_2") + -- Cursor stays on the same item + assert.equals(12, vim.api.nvim_win_get_cursor(0)[1]) + vim.api.nvim_win_set_cursor(0, { 14, 0 }) + + -- Expanding again will produce the same result + quicker.expand() + test_util.assert_snapshot(0, "expand_2") + assert.equals(14, vim.api.nvim_win_get_cursor(0)[1]) + + -- Expanding again will produce the same result + quicker.expand({ add_to_existing = true }) + test_util.assert_snapshot(0, "expand_3") + + -- Collapsing will return to the original state + quicker.collapse() + test_util.assert_snapshot(0, "expand_1") + assert.equals(3, vim.api.nvim_win_get_cursor(0)[1]) + end) + + it("expand loclist results", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("expand_loclist.txt", 10)) + vim.fn.setloclist(0, { + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + valid = 1, + }, + }) + vim.cmd.lopen() + quicker.expand() + test_util.assert_snapshot(0, "expand_loclist") + end) + + it("expand when items missing bufnr", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("expand_missing.txt", 10)) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + valid = 1, + }, + { + text = "Valid line with no bufnr", + lnum = 4, + valid = 1, + }, + { + bufnr = bufnr, + text = "Invalid line with a bufnr", + lnum = 5, + valid = 0, + }, + { + text = "Invalid line with no bufnr", + lnum = 6, + valid = 0, + }, + }) + vim.cmd.copen() + quicker.expand() + -- The last three lines should be stripped after expansion + test_util.assert_snapshot(0, "expand_missing") + end) + + it("expand removes duplicate line entries", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("expand_dupe.txt", 10)) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + valid = 1, + }, + { + bufnr = bufnr, + text = "line 3", + lnum = 3, + valid = 1, + }, + { + bufnr = bufnr, + text = "line 3", + lnum = 3, + valid = 1, + }, + }) + vim.cmd.copen() + test_util.assert_snapshot(0, "expand_dupe_1") + + quicker.expand() + test_util.assert_snapshot(0, "expand_dupe_2") + end) +end) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/display_spec.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/display_spec.lua new file mode 100644 index 0000000..c3404ad --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/display_spec.lua @@ -0,0 +1,145 @@ +require("plenary.async").tests.add_to_env() +local config = require("quicker.config") +local test_util = require("tests.test_util") + +local sleep = require("plenary.async.util").sleep + +a.describe("display", function() + after_each(function() + test_util.reset_editor() + end) + + it("renders quickfix items", function() + vim.fn.setqflist({ + { + bufnr = vim.fn.bufadd("README.md"), + text = "text", + lnum = 5, + valid = 1, + }, + { + filename = "README.md", + text = "text", + lnum = 10, + col = 0, + end_col = 4, + nr = 3, + type = "E", + valid = 1, + }, + { + module = "mod", + bufnr = vim.fn.bufadd("README.md"), + text = "text", + valid = 1, + }, + { + bufnr = vim.fn.bufadd("README.md"), + text = "text", + valid = 0, + }, + { + bufnr = vim.fn.bufadd("README.md"), + lnum = 1, + text = "", + valid = 0, + }, + }) + vim.cmd.copen() + test_util.assert_snapshot(0, "display_1") + end) + + a.it("truncates long filenames", function() + config.max_filename_width = function() + return 10 + end + local bufnr = vim.fn.bufadd(test_util.make_tmp_file(string.rep("f", 10) .. ".txt", 10)) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "text", + lnum = 5, + valid = 1, + }, + }) + vim.cmd.copen() + -- Wait for highlights to be applied + sleep(50) + test_util.assert_snapshot(0, "display_long_1") + end) + + a.it("renders minimal line when no filenames in results", function() + vim.fn.setqflist({ + { + text = "text", + }, + }) + vim.cmd.copen() + -- Wait for highlights to be applied + sleep(50) + test_util.assert_snapshot(0, "display_minimal_1") + end) + + a.it("sets signs for diagnostics", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("sign_test.txt", 10)) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "text", + lnum = 1, + type = "E", + valid = 1, + }, + { + bufnr = bufnr, + text = "text", + lnum = 2, + type = "W", + valid = 1, + }, + { + bufnr = bufnr, + text = "text", + lnum = 3, + type = "I", + valid = 1, + }, + { + bufnr = bufnr, + text = "text", + lnum = 4, + type = "H", + valid = 1, + }, + { + bufnr = bufnr, + text = "text", + lnum = 5, + type = "N", + valid = 1, + }, + }) + vim.cmd.copen() + + -- Wait for highlights to be applied + sleep(50) + local ns = vim.api.nvim_create_namespace("quicker_highlights") + local marks = vim.api.nvim_buf_get_extmarks(0, ns, 0, -1, { type = "sign" }) + assert.equals(5, #marks) + local expected = { + { "DiagnosticSignError", config.type_icons.E }, + { "DiagnosticSignWarn", config.type_icons.W }, + { "DiagnosticSignInfo", config.type_icons.I }, + { "DiagnosticSignHint", config.type_icons.H }, + { "DiagnosticSignHint", config.type_icons.N }, + } + for i, mark_data in ipairs(marks) do + local extmark_id, row = mark_data[1], mark_data[2] + local mark = vim.api.nvim_buf_get_extmark_by_id(0, ns, extmark_id, { details = true }) + local hl_group, icon = unpack(expected[i]) + assert.equals(i - 1, row) + assert.equals(hl_group, mark[3].sign_hl_group) + assert.equals(icon, mark[3].sign_text) + end + end) +end) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/editor_spec.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/editor_spec.lua new file mode 100644 index 0000000..0999508 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/editor_spec.lua @@ -0,0 +1,347 @@ +local config = require("quicker.config") +local display = require("quicker.display") +local quicker = require("quicker") +local test_util = require("tests.test_util") + +---@param lnum integer +---@param line string +local function replace_text(lnum, line) + local prev_line = vim.api.nvim_buf_get_lines(0, lnum - 1, lnum, true)[1] + local idx = prev_line:find(display.EM_QUAD, 1, true) + vim.api.nvim_buf_set_text(0, lnum - 1, idx + display.EM_QUAD_LEN - 1, lnum - 1, -1, { line }) +end + +---@param lnum integer +local function del_line(lnum) + vim.cmd.normal({ args = { string.format("%dggdd", lnum) }, bang = true }) +end + +local function wait_virt_text() + vim.wait(10, function() + return false + end) +end + +describe("editor", function() + after_each(function() + test_util.reset_editor() + end) + + it("can edit one line in file", function() + vim.cmd.edit({ args = { test_util.make_tmp_file("edit_1.txt", 10) } }) + local bufnr = vim.api.nvim_get_current_buf() + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + }, + }) + vim.cmd.copen() + wait_virt_text() + replace_text(1, "new text") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_1") + end) + + it("can edit across multiple files", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("edit_multiple_1.txt", 10)) + vim.fn.bufload(bufnr) + local buf2 = vim.fn.bufadd(test_util.make_tmp_file("edit_multiple_2.txt", 10)) + vim.fn.bufload(buf2) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + }, + { + bufnr = bufnr, + text = "line 9", + lnum = 9, + }, + { + bufnr = buf2, + text = "line 5", + lnum = 5, + }, + }) + vim.cmd.copen() + quicker.expand() + wait_virt_text() + replace_text(2, "new text") + replace_text(3, "some text") + replace_text(7, "other text") + replace_text(11, "final text") + local last_line = vim.api.nvim_buf_line_count(0) + vim.api.nvim_win_set_cursor(0, { last_line, 0 }) + vim.cmd.write() + test_util.assert_snapshot(0, "edit_multiple_qf") + test_util.assert_snapshot(bufnr, "edit_multiple_1") + test_util.assert_snapshot(buf2, "edit_multiple_2") + -- We should keep the cursor position + assert.equals(last_line, vim.api.nvim_win_get_cursor(0)[1]) + end) + + it("can expand then edit expanded line", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("edit_expanded.txt", 10)) + vim.fn.bufload(bufnr) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + }, + }) + vim.cmd.copen() + quicker.expand() + wait_virt_text() + replace_text(1, "first") + replace_text(2, "second") + replace_text(3, "third") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_expanded") + test_util.assert_snapshot(0, "edit_expanded_qf") + end) + + it("fails when source text is different", function() + vim.cmd.edit({ args = { test_util.make_tmp_file("edit_fail.txt", 10) } }) + local bufnr = vim.api.nvim_get_current_buf() + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "buzz buzz", + lnum = 2, + }, + }) + vim.cmd.copen() + wait_virt_text() + replace_text(1, "new text") + test_util.with(function() + local notify = vim.notify + ---@diagnostic disable-next-line: duplicate-set-field + vim.notify = function() end + return function() + vim.notify = notify + end + end, function() + vim.cmd.write() + end) + test_util.assert_snapshot(bufnr, "edit_fail") + test_util.assert_snapshot(0, "edit_fail_qf") + end) + + it("can handle multiple qf items on same lnum", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("edit_dupe.txt", 10)) + vim.fn.bufload(bufnr) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + col = 0, + }, + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + col = 3, + }, + }) + vim.cmd.copen() + wait_virt_text() + replace_text(1, "first") + replace_text(2, "second") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_dupe") + test_util.assert_snapshot(0, "edit_dupe_qf") + + -- If only one of them has a change, it should go through + replace_text(1, "line 2") + replace_text(2, "second") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_dupe_2") + test_util.assert_snapshot(0, "edit_dupe_qf_2") + end) + + it("handles deleting lines (shrinks quickfix)", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("edit_delete.txt", 10)) + vim.fn.bufload(bufnr) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + }, + { + bufnr = bufnr, + text = "line 3", + lnum = 3, + }, + { + bufnr = bufnr, + text = "line 6", + lnum = 6, + }, + }) + vim.cmd.copen() + wait_virt_text() + del_line(3) + del_line(2) + vim.cmd.write() + assert.are.same({ + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + col = 0, + end_col = 0, + vcol = 0, + end_lnum = 0, + module = "", + nr = 0, + pattern = "", + type = "", + valid = 1, + }, + }, vim.fn.getqflist()) + end) + + it("handles loclist", function() + vim.cmd.edit({ args = { test_util.make_tmp_file("edit_ll.txt", 10) } }) + local bufnr = vim.api.nvim_get_current_buf() + vim.fn.setloclist(0, { + { + bufnr = bufnr, + text = "line 2", + lnum = 2, + }, + }) + vim.cmd.lopen() + wait_virt_text() + replace_text(1, "new text") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_ll") + end) + + it("handles text that contains the delimiter", function() + vim.cmd.edit({ args = { test_util.make_tmp_file("edit_delim.txt", 10) } }) + local bufnr = vim.api.nvim_get_current_buf() + local line = "line 2 " .. config.borders.vert .. " text" + vim.api.nvim_buf_set_lines(bufnr, 1, 2, false, { line }) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = line, + lnum = 2, + }, + }) + vim.cmd.copen() + wait_virt_text() + replace_text(1, line .. " " .. config.borders.vert .. " more text") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_delim") + end) + + it("can edit lines with trimmed common whitespace", function() + require("quicker.config").trim_leading_whitespace = "common" + vim.cmd.edit({ + args = { + test_util.make_tmp_file("edit_whitespace.txt", { + " line 1", + " line 2", + " line 3", + " line 4", + }), + }, + }) + local bufnr = vim.api.nvim_get_current_buf() + vim.fn.setqflist({ + { + bufnr = bufnr, + text = " line 2", + lnum = 2, + }, + { + bufnr = bufnr, + text = " line 3", + lnum = 3, + }, + }) + vim.cmd.copen() + wait_virt_text() + test_util.assert_snapshot(0, "edit_whitespace_qf") + replace_text(1, "foo") + replace_text(2, "bar") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_whitespace") + end) + + it("can edit lines with trimmed all whitespace", function() + require("quicker.config").trim_leading_whitespace = "all" + vim.cmd.edit({ + args = { + test_util.make_tmp_file("edit_whitespace.txt", { + " line 1", + " line 2", + " line 3", + " line 4", + }), + }, + }) + local bufnr = vim.api.nvim_get_current_buf() + vim.fn.setqflist({ + { + bufnr = bufnr, + text = " line 2", + lnum = 2, + }, + { + bufnr = bufnr, + text = " line 3", + lnum = 3, + }, + }) + vim.cmd.copen() + wait_virt_text() + test_util.assert_snapshot(0, "edit_all_whitespace_qf") + replace_text(1, "foo") + replace_text(2, "bar") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_all_whitespace") + end) + + it("can edit lines with untrimmed whitespace", function() + require("quicker.config").trim_leading_whitespace = false + vim.cmd.edit({ + args = { + test_util.make_tmp_file("edit_whitespace.txt", { + " line 1", + " line 2", + " line 3", + " line 4", + }), + }, + }) + local bufnr = vim.api.nvim_get_current_buf() + vim.fn.setqflist({ + { + bufnr = bufnr, + text = " line 2", + lnum = 2, + }, + { + bufnr = bufnr, + text = " line 3", + lnum = 3, + }, + }) + vim.cmd.copen() + wait_virt_text() + test_util.assert_snapshot(0, "edit_none_whitespace_qf") + replace_text(1, "foo") + replace_text(2, "bar") + vim.cmd.write() + test_util.assert_snapshot(bufnr, "edit_none_whitespace") + end) +end) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/fs_spec.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/fs_spec.lua new file mode 100644 index 0000000..2e1e54f --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/fs_spec.lua @@ -0,0 +1,20 @@ +local fs = require("quicker.fs") + +local home = os.getenv("HOME") +local cwd = vim.fn.getcwd() + +describe("fs", function() + it("shortens path", function() + assert.equals("~/bar/baz.txt", fs.shorten_path(home .. "/bar/baz.txt")) + assert.equals("bar/baz.txt", fs.shorten_path(cwd .. "/bar/baz.txt")) + assert.equals("/foo/bar.txt", fs.shorten_path("/foo/bar.txt")) + end) + + it("finds subpath", function() + assert.truthy(fs.is_subpath("/root", "/root/foo")) + assert.truthy(fs.is_subpath(cwd, "foo")) + assert.falsy(fs.is_subpath("/root", "/foo")) + assert.falsy(fs.is_subpath("/root", "/rooter/foo")) + assert.falsy(fs.is_subpath("/root", "/root/../foo")) + end) +end) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/minimal_init.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/minimal_init.lua new file mode 100644 index 0000000..486b213 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/minimal_init.lua @@ -0,0 +1,16 @@ +vim.cmd([[set runtimepath+=.]]) + +vim.o.swapfile = false +vim.bo.swapfile = false +require("tests.test_util").reset_editor() + +-- TODO test highlighting (both highlight.lua module and adding them in display.lua) +-- TODO test syntax highlighting when customizing delimiter + +vim.api.nvim_create_user_command("RunTests", function(opts) + local path = opts.fargs[1] or "tests" + require("plenary.test_harness").test_directory( + path, + { minimal_init = "./tests/minimal_init.lua" } + ) +end, { nargs = "?" }) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/opts_spec.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/opts_spec.lua new file mode 100644 index 0000000..0732da2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/opts_spec.lua @@ -0,0 +1,52 @@ +local quicker = require("quicker") +local test_util = require("tests.test_util") + +describe("opts", function() + after_each(function() + test_util.reset_editor() + end) + + it("sets buffer opts", function() + quicker.setup({ + opts = { + buflisted = true, + bufhidden = "wipe", + cindent = true, + }, + }) + vim.fn.setqflist({ + { + bufnr = vim.fn.bufadd("README.md"), + text = "text", + lnum = 5, + valid = 1, + }, + }) + vim.cmd.copen() + assert.truthy(vim.bo.buflisted) + assert.equals("wipe", vim.bo.bufhidden) + assert.truthy(vim.bo.cindent) + end) + + it("sets window opts", function() + quicker.setup({ + opts = { + wrap = false, + number = true, + list = true, + }, + }) + vim.fn.setqflist({ + { + bufnr = vim.fn.bufadd("README.md"), + text = "text", + lnum = 5, + valid = 1, + }, + }) + vim.cmd.copen() + assert.falsy(vim.wo.wrap) + assert.truthy(vim.wo.number) + assert.truthy(vim.wo.list) + end) +end) diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_1 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_1 new file mode 100644 index 0000000..270a164 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_1 @@ -0,0 +1,5 @@ +README.md ┃ 5┃text +README.md ┃10┃text +mod ┃ ┃text +README.md ┃ ┃text +README.md ┃ 1┃
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_long_1 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_long_1 new file mode 100644 index 0000000..d585beb --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_long_1 @@ -0,0 +1 @@ +…ffffffff.txt ┃ 5┃text
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_minimal_1 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_minimal_1 new file mode 100644 index 0000000..46190e7 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/display_minimal_1 @@ -0,0 +1 @@ + ┃text
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_1 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_1 new file mode 100644 index 0000000..a18ed5a --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_1 @@ -0,0 +1,10 @@ +line 1 +new text +line 3 +line 4 +line 5 +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_all_whitespace b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_all_whitespace new file mode 100644 index 0000000..998c877 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_all_whitespace @@ -0,0 +1,4 @@ + line 1 + foo + bar + line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_all_whitespace_qf b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_all_whitespace_qf new file mode 100644 index 0000000..baf8533 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_all_whitespace_qf @@ -0,0 +1,2 @@ +tests/tmp/edit_whitespace.txt ┃ 2┃line 2 +tests/tmp/edit_whitespace.txt ┃ 3┃line 3
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_delim b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_delim new file mode 100644 index 0000000..75a9e7f --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_delim @@ -0,0 +1,10 @@ +line 1 +line 2 ┃ text ┃ more text +line 3 +line 4 +line 5 +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe new file mode 100644 index 0000000..b3f56ae --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe @@ -0,0 +1,10 @@ +line 1 +line 2 +line 3 +line 4 +line 5 +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_2 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_2 new file mode 100644 index 0000000..3ae9ccc --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_2 @@ -0,0 +1,10 @@ +line 1 +second +line 3 +line 4 +line 5 +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_qf b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_qf new file mode 100644 index 0000000..7e01207 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_qf @@ -0,0 +1,2 @@ +tests/tmp/edit_dupe.txt ┃ 2┃first +tests/tmp/edit_dupe.txt ┃ 2┃second
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_qf_2 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_qf_2 new file mode 100644 index 0000000..1acfd8e --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_dupe_qf_2 @@ -0,0 +1,2 @@ +tests/tmp/edit_dupe.txt ┃ 2┃line 2 +tests/tmp/edit_dupe.txt ┃ 2┃second
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_expanded b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_expanded new file mode 100644 index 0000000..afc39ad --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_expanded @@ -0,0 +1,10 @@ +first +second +third +line 4 +line 5 +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_expanded_qf b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_expanded_qf new file mode 100644 index 0000000..991dd06 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_expanded_qf @@ -0,0 +1,4 @@ + ┃ 1┃first +tests/tmp/edit_expanded.txt ┃ 2┃second + ┃ 3┃third + ┃ 4┃line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_fail b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_fail new file mode 100644 index 0000000..b3f56ae --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_fail @@ -0,0 +1,10 @@ +line 1 +line 2 +line 3 +line 4 +line 5 +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_fail_qf b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_fail_qf new file mode 100644 index 0000000..3eb9f10 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_fail_qf @@ -0,0 +1 @@ +tests/tmp/edit_fail.txt ┃ 2┃new text
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_invalid b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_invalid new file mode 100644 index 0000000..386c994 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_invalid @@ -0,0 +1 @@ + ┃ ┃new text diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_ll b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_ll new file mode 100644 index 0000000..a18ed5a --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_ll @@ -0,0 +1,10 @@ +line 1 +new text +line 3 +line 4 +line 5 +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_1 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_1 new file mode 100644 index 0000000..765403a --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_1 @@ -0,0 +1,10 @@ +line 1 +new text +some text +line 4 +line 5 +line 6 +line 7 +line 8 +other text +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_2 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_2 new file mode 100644 index 0000000..c988ea1 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_2 @@ -0,0 +1,10 @@ +line 1 +line 2 +line 3 +line 4 +final text +line 6 +line 7 +line 8 +line 9 +line 10
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_qf b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_qf new file mode 100644 index 0000000..3de41ad --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_multiple_qf @@ -0,0 +1,15 @@ + ┃ 1┃line 1 +tests/tmp/edit_multiple_1.txt ┃ 2┃new text + ┃ 3┃some text + ┃ 4┃line 4 +╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╂╌╌╂╌╌╌╌╌╌╌╌ + ┃ 7┃line 7 + ┃ 8┃line 8 +tests/tmp/edit_multiple_1.txt ┃ 9┃other text + ┃10┃line 10 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━╋━━━━━━━━ + ┃ 3┃line 3 + ┃ 4┃line 4 +tests/tmp/edit_multiple_2.txt ┃ 5┃final text + ┃ 6┃line 6 + ┃ 7┃line 7
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_none_whitespace b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_none_whitespace new file mode 100644 index 0000000..4b592c2 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_none_whitespace @@ -0,0 +1,4 @@ + line 1 +foo +bar + line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_none_whitespace_qf b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_none_whitespace_qf new file mode 100644 index 0000000..5be47f1 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_none_whitespace_qf @@ -0,0 +1,2 @@ +tests/tmp/edit_whitespace.txt ┃ 2┃ line 2 +tests/tmp/edit_whitespace.txt ┃ 3┃ line 3
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_whitespace b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_whitespace new file mode 100644 index 0000000..6a5ca4b --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_whitespace @@ -0,0 +1,4 @@ + line 1 + foo + bar + line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_whitespace_qf b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_whitespace_qf new file mode 100644 index 0000000..e26d928 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/edit_whitespace_qf @@ -0,0 +1,2 @@ +tests/tmp/edit_whitespace.txt ┃ 2┃line 2 +tests/tmp/edit_whitespace.txt ┃ 3┃ line 3
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_1 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_1 new file mode 100644 index 0000000..ab1901f --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_1 @@ -0,0 +1,3 @@ +tests/tmp/expand_1.txt ┃ 2┃line 2 +tests/tmp/expand_1.txt ┃ 8┃line 8 +tests/tmp/expand_2.txt ┃ 4┃line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_2 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_2 new file mode 100644 index 0000000..e0a2139 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_2 @@ -0,0 +1,16 @@ + ┃ 1┃line 1 +tests/tmp/expand_1.txt ┃ 2┃line 2 + ┃ 3┃line 3 + ┃ 4┃line 4 +╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╂╌╌╂╌╌╌╌╌╌╌╌ + ┃ 6┃line 6 + ┃ 7┃line 7 +tests/tmp/expand_1.txt ┃ 8┃line 8 + ┃ 9┃line 9 + ┃10┃line 10 +━━━━━━━━━━━━━━━━━━━━━━━╋━━╋━━━━━━━━ + ┃ 2┃line 2 + ┃ 3┃line 3 +tests/tmp/expand_2.txt ┃ 4┃line 4 + ┃ 5┃line 5 + ┃ 6┃line 6
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_3 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_3 new file mode 100644 index 0000000..0a20a1c --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_3 @@ -0,0 +1,19 @@ + ┃ 1┃line 1 +tests/tmp/expand_1.txt ┃ 2┃line 2 + ┃ 3┃line 3 + ┃ 4┃line 4 + ┃ 5┃line 5 + ┃ 6┃line 6 + ┃ 7┃line 7 +tests/tmp/expand_1.txt ┃ 8┃line 8 + ┃ 9┃line 9 + ┃10┃line 10 +━━━━━━━━━━━━━━━━━━━━━━━╋━━╋━━━━━━━━ + ┃ 1┃line 1 + ┃ 2┃line 2 + ┃ 3┃line 3 +tests/tmp/expand_2.txt ┃ 4┃line 4 + ┃ 5┃line 5 + ┃ 6┃line 6 + ┃ 7┃line 7 + ┃ 8┃line 8
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_dupe_1 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_dupe_1 new file mode 100644 index 0000000..8e32cb4 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_dupe_1 @@ -0,0 +1,3 @@ +tests/tmp/expand_dupe.txt ┃ 2┃line 2 +tests/tmp/expand_dupe.txt ┃ 3┃line 3 +tests/tmp/expand_dupe.txt ┃ 3┃line 3
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_dupe_2 b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_dupe_2 new file mode 100644 index 0000000..b51efa8 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_dupe_2 @@ -0,0 +1,5 @@ + ┃ 1┃line 1 +tests/tmp/expand_dupe.txt ┃ 2┃line 2 +tests/tmp/expand_dupe.txt ┃ 3┃line 3 + ┃ 4┃line 4 + ┃ 5┃line 5
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_loclist b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_loclist new file mode 100644 index 0000000..66a6207 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_loclist @@ -0,0 +1,4 @@ + ┃ 1┃line 1 +tests/tmp/expand_loclist.txt ┃ 2┃line 2 + ┃ 3┃line 3 + ┃ 4┃line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_missing b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_missing new file mode 100644 index 0000000..f29a273 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/expand_missing @@ -0,0 +1,4 @@ + ┃ 1┃line 1 +tests/tmp/expand_missing.txt ┃ 2┃line 2 + ┃ 3┃line 3 + ┃ 4┃line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_all_whitespace b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_all_whitespace new file mode 100644 index 0000000..e664f80 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_all_whitespace @@ -0,0 +1,2 @@ +tests/tmp/whitespace_1.txt ┃ 2┃line 2 +tests/tmp/whitespace_1.txt ┃ 3┃line 3
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_mixed_whitespace b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_mixed_whitespace new file mode 100644 index 0000000..f6464d4 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_mixed_whitespace @@ -0,0 +1,2 @@ +tests/tmp/mixed_whitespace.txt ┃ 1┃ line 1 +tests/tmp/mixed_whitespace.txt ┃ 2┃ line 2
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_whitespace b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_whitespace new file mode 100644 index 0000000..49a6e20 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_whitespace @@ -0,0 +1,2 @@ +tests/tmp/whitespace.txt ┃ 2┃line 2 +tests/tmp/whitespace.txt ┃ 3┃ line 3
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_whitespace_expanded b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_whitespace_expanded new file mode 100644 index 0000000..07b70d1 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/snapshots/trim_whitespace_expanded @@ -0,0 +1,5 @@ + ┃ 1┃ line 1 +tests/tmp/whitespace.txt ┃ 2┃line 2 +tests/tmp/whitespace.txt ┃ 3┃ line 3 + ┃ 4┃ + ┃ 5┃ line 4
\ No newline at end of file diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/test_util.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/test_util.lua new file mode 100644 index 0000000..94b0a40 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/test_util.lua @@ -0,0 +1,142 @@ +require("plenary.async").tests.add_to_env() +local M = {} + +local tmp_files = {} +M.reset_editor = function() + vim.cmd.tabonly({ mods = { silent = true } }) + for i, winid in ipairs(vim.api.nvim_tabpage_list_wins(0)) do + if i > 1 then + vim.api.nvim_win_close(winid, true) + end + end + vim.api.nvim_win_set_buf(0, vim.api.nvim_create_buf(false, true)) + for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do + vim.api.nvim_buf_delete(bufnr, { force = true }) + end + vim.fn.setqflist({}) + vim.fn.setloclist(0, {}) + for _, filename in ipairs(tmp_files) do + vim.uv.fs_unlink(filename) + end + tmp_files = {} + + require("quicker").setup({ + header_length = function() + -- Make this deterministic so the snapshots are stable + return 8 + end, + }) +end + +---@param basename string +---@param lines integer|string[] +---@return string +M.make_tmp_file = function(basename, lines) + vim.fn.mkdir("tests/tmp", "p") + local filename = "tests/tmp/" .. basename + table.insert(tmp_files, filename) + local f = assert(io.open(filename, "w")) + if type(lines) == "table" then + for _, line in ipairs(lines) do + f:write(line .. "\n") + end + else + for i = 1, lines do + f:write("line " .. i .. "\n") + end + end + f:close() + return filename +end + +---@param name string +---@return string[] +local function load_snapshot(name) + local path = "tests/snapshots/" .. name + if vim.fn.filereadable(path) == 0 then + return {} + end + local f = assert(io.open(path, "r")) + local lines = {} + for line in f:lines() do + table.insert(lines, line) + end + f:close() + return lines +end + +---@param name string +---@param lines string[] +local function save_snapshot(name, lines) + vim.fn.mkdir("tests/snapshots", "p") + local path = "tests/snapshots/" .. name + local f = assert(io.open(path, "w")) + f:write(table.concat(lines, "\n")) + f:close() + return lines +end + +---@param bufnr integer +---@param name string +M.assert_snapshot = function(bufnr, name) + -- Wait for the virtual text extmarks to be set + if vim.bo[bufnr].filetype == "qf" then + vim.wait(10, function() + return false + end) + end + local util = require("quicker.util") + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + + -- Add virtual text to lines + local headers = {} + local header_ns = vim.api.nvim_create_namespace("quicker_headers") + for i, v in ipairs(lines) do + local extmarks = util.get_lnum_extmarks(bufnr, i, v:len()) + assert(#extmarks <= 1, "Expected at most one extmark per line") + local mark = extmarks[1] + if mark then + local start_col = mark[3] + local data = mark[4] + local virt_text = table.concat( + vim.tbl_map(function(vt) + return vt[1] + end, data.virt_text), + "" + ) + lines[i] = v:sub(0, start_col) .. virt_text .. v:sub(start_col + 1) + + extmarks = util.get_lnum_extmarks(bufnr, i, v:len(), header_ns) + assert(#extmarks <= 1, "Expected at most one extmark per line") + mark = extmarks[1] + if mark and mark[4].virt_lines then + table.insert(headers, { i, mark[4].virt_lines[1][1][1] }) + end + end + end + + for i = #headers, 1, -1 do + local lnum, header = unpack(headers[i]) + table.insert(lines, lnum, header) + end + + if os.getenv("UPDATE_SNAPSHOTS") then + save_snapshot(name, lines) + else + local expected = load_snapshot(name) + assert.are.same(expected, lines) + end +end + +---@param context fun(): fun() +---@param fn fun() +M.with = function(context, fn) + local cleanup = context() + local ok, err = pcall(fn) + cleanup() + if not ok then + error(err) + end +end + +return M diff --git a/mut/neovim/pack/plugins/start/quicker.nvim/tests/whitespace_spec.lua b/mut/neovim/pack/plugins/start/quicker.nvim/tests/whitespace_spec.lua new file mode 100644 index 0000000..0933276 --- /dev/null +++ b/mut/neovim/pack/plugins/start/quicker.nvim/tests/whitespace_spec.lua @@ -0,0 +1,83 @@ +local quicker = require("quicker") +local test_util = require("tests.test_util") + +describe("whitespace", function() + before_each(function() + require("quicker.config").trim_leading_whitespace = "common" + end) + after_each(function() + test_util.reset_editor() + end) + + it("removes common leading whitespace from valid results", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("whitespace.txt", { + " line 1", + " line 2", + " line 3", + "", + " line 4", + })) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = " line 2", + lnum = 2, + }, + { + bufnr = bufnr, + text = " line 3", + lnum = 3, + }, + }) + vim.cmd.copen() + test_util.assert_snapshot(0, "trim_whitespace") + quicker.expand() + test_util.assert_snapshot(0, "trim_whitespace_expanded") + end) + + it("handles mixed tabs and spaces", function() + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("mixed_whitespace.txt", { + " line 1", + "\t\tline 2", + })) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = " line 1", + lnum = 1, + }, + { + bufnr = bufnr, + text = "\t\tline 2", + lnum = 2, + }, + }) + vim.cmd.copen() + test_util.assert_snapshot(0, "trim_mixed_whitespace") + end) + + it("removes all leading whitespace", function() + require("quicker.config").trim_leading_whitespace = "all" + local bufnr = vim.fn.bufadd(test_util.make_tmp_file("whitespace_1.txt", { + " line 1", + " line 2", + " line 3", + "", + " line 4", + })) + vim.fn.setqflist({ + { + bufnr = bufnr, + text = " line 2", + lnum = 2, + }, + { + bufnr = bufnr, + text = " line 3", + lnum = 3, + }, + }) + vim.cmd.copen() + test_util.assert_snapshot(0, "trim_all_whitespace") + end) +end) |
